出口契约认藏在代码块里的正文改为逐行标出代码:写明是文字的块不再因为一行 SQL、一行像 YAML 的键或 + / - 要点就整块当代码,公式、JSON、目录树这类像数据的行照代码看

上一轮先判整块:只要有一行以 SQL / Python 关键字或小写键开头,或者每行都以 + - 空格 @ 开头,整块就当代码,只认去掉
注释、字符串之后还成句的行。写成 markdown、text 的块里,+ / - 开头的正负面要点、正文后面另嵌的 ~~~sql、缩进的 select、
一行「- note: …」都让不带句读的结论认不出,藏进代码块反倒完整出具。反过来,写明是文字的块不去字符串,六字连写、结论词
又对公式、JSON、目录树、print、流程箭头这些行生效:结论句为 0 的报告贴一段带「占比」的公式就降档,缺口还说正文写在
代码块里。

- 逐行标出代码(_code_lines),不再整块判:块里再嵌、收得了的代码块(~~~sql、````,开了没收的那一行照普通的行看);
  SQL / Python 语句从开头那一行到这一段结束,续行也算;整段是 YAML 的(第一行是小写键,其余是键或缩进的行;写明是
  文字的块里另要有值为空的键,一行一个「- note: 一句话」是正文里的标签);像数据、公式的行(前面有字的等号和箭头、
  画目录树的制表符、JSON 的开头和「"键":」、函数调用、文件名、逗号隔开的表头)。代码的行只认去掉注释、字符串之后
  成句的,别的行照旧:写明是文字的按 _written,信息串为空的按 _prose_line
- diff:写明是文字的块里 + - 后面空一格的是列表项,紧挨着字写的才是 diff;信息串为空的块里第一个改动行是 + 的是
  正负面要点(diff 先列删掉的行),有 @@ 块头的照旧是 diff
- 一段到空行为止,只写着块注释的行不算空行;写明是文字的块里顶格的标题另起一段。块注释、三个引号括起的多行字符串
  涂成空格,行数不变
- SQL 关键字只认全小写、全大写,def / class 要写成定义的样子:「From 数据看……」「class A 门店……」是正文。代码的行里
  -- // 紧挨着前面的字也算注释
- 取舍写进 _fenced_body 的说明:一块里只要有一行在写正文就记缺口,拿不准的行宁可当代码,藏起来的正文总有别的行认得出

测试:复核里的写法逐一补上:+ / - 要点(markdown、text、信息串为空)、嵌 ~~~sql、缩进的 select、小写键的标签,正式
运行降档并只带这条缺口;带「占比」「增长率」的公式、写成 text 的 JSON 和目录树,在结论句为 0 的报告里照常完整出具。
单元参数另补 print、groupby、流程箭头、文件名、逗号表头、JSON 数组、多行 SQL 夹块注释、docstring、嵌套的 YAML、开了
没收和收尾更长的嵌套围栏、行首箭头、首字母大写的关键字、先列 + 的带 @@ diff,并钉住两条取舍:写成 text 又空一格的
diff 照列表看;信息串为空、整段都是小写键的照 YAML 看。长行耗时用例扩到七种长行、两种多行围栏、两种信息串。逐一去掉
36 处规则(diff、嵌套围栏、语句、关键字大小写、def / class、YAML、分段、像数据的七种写法、注释、块注释、多行字符串、
正文判法、结构片段、挂钩),各有用例变红。
This commit is contained in:
ThearchyHelios
2026-10-06 19:51:29 +08:00
parent 46403a0aac
commit 6d9d31395b
2 changed files with 271 additions and 62 deletions
+134 -42
View File
@@ -501,20 +501,37 @@ _CJK_RUN = re.compile(r"[\u4e00-\u9fff]{6,}")
#: 英文的词串:单词之间只隔空格、逗号。五个以上的单词紧跟句末标点算一句(_en_sentence)
_EN_RUN = re.compile(r"[A-Za-z]+(?:[ ,]+[A-Za-z]+)*")
_EN_WORD = re.compile(r"[A-Za-z]+")
#: Markdown 标题:正文里的标题切块时记为 heading,从不算结论句(evidence._scan_blocks),代码块里的也不算在写正文
_HEADING = re.compile(r"\s*#{1,6}\s")
#: 整行是注释(-- // # /* */ 开头;# 开头的 Markdown 标题本来也不是句子),和行尾隔着空白的注释:写着中文句子也是
#: 代码的一部分
#: 整行是注释(-- // # /* */ 开头),和行尾隔着空白的注释:写着中文句子也是代码的一部分。# 开头的 Markdown 标题本来
#: 也不算结论句(evidence._scan_blocks 把它切成 heading)
_COMMENT_LINE = re.compile(r"\s*(?:--|//|#|/\*|\*/)")
_TRAILING_COMMENT = re.compile(r"\s(?:--|//|#).*")
#: 看得出是代码的行里,-- // 紧挨着前面的字也是注释(SELECT SUM(amount)--按区域汇总):字符串已经先去掉了
_CODE_COMMENT = re.compile(r"(?:--|//|\s#).*")
#: 写明是文字的块里顶格写的 Markdown 标题:标题前后是两段(_code_lines)
_MD_HEADING = re.compile(r"#{1,6}\s")
#: 代码里的字符串字面量('…'、"…",可带 f、r、b、u、N 这类前缀):里面的中文标点是数据。引号前面紧挨着字母、数字的
#: 不算开头(英文的 it's、don't)
_STRING_LITERAL = re.compile(r"(?<![A-Za-z0-9_])[rRbBuUfFnN]{0,2}(?:'[^'\n]*'|\"[^\"\n]*\")")
#: 看得出是代码的行:SQL、Python 的开头,YAML 的键(「columns:」「- name: 退款占比」)。YAML 的键只认小写开头的:
#: 正文里的「Note:」「Q1:」是标签
_CODE_LINE = re.compile(r"\s*(?:select|with|from|where|join|group\s+by|order\s+by|having|union|insert|update"
r"|delete|create|def|class|import|return)\b", re.IGNORECASE)
#: 可以跨行的块注释、多行字符串:开头的写法,和它对应的收尾
_SPAN_OPEN = re.compile(r"/\*|\"\"\"|'''")
_SPAN_CLOSE = {"/*": "*/", '"""': '"""', "'''": "'''"}
#: SQL、Python 语句的开头。SQL 的关键字只认全小写、全大写的写法:「From 数据看……」「Update ……」这类首字母大写的是
#: 正文里的英文词;def、class 要写成定义的样子(def 名字(、class 名字: ),「class A 门店……」说的是门店的分类
_CODE_LINE = re.compile(r"\s*(?:(?:select|with|from|where|join|group\s+by|order\s+by|having|union|insert|update|delete"
r"|create|import|return|SELECT|WITH|FROM|WHERE|JOIN|GROUP\s+BY|ORDER\s+BY|HAVING|UNION|INSERT"
r"|UPDATE|DELETE|CREATE)\b|(?:def|class)\s+[A-Za-z_]\w*\s*[(:])")
#: YAML 的键(「columns:」「- name: 退款占比」),只认小写开头的:正文里的「Note:」「Q1:」是标签。值为空、下面缩进着
#: 列表或子键的另记一种(_YAML_NEST)
_YAML_KEY = re.compile(r"\s*(?:-\s+)?[a-z_][\w.-]*:(?:\s|$)")
_YAML_NEST = re.compile(r"\s*(?:-\s+)?[a-z_][\w.-]*:\s*$")
#: 块里再嵌的代码块:~~~ 或三个以上反引号开头。compose_doc 只认整行三个反引号的围栏,那样的一行外层就收了,块里剩下的
#: 嵌套写法是 ~~~sql、````sql 这些
_NESTED_FENCE = re.compile(r"\s*(`{3,}|~{3,})(.*)")
#: 像数据、公式的一行:前面有字的等号(公式、赋值、比较)和箭头(连起的流程),画目录树的制表符,JSON 的对象、数组开头
#: ({"、[")和「"键":」,函数调用(print(、groupby(),带扩展名的文件名(周报.xlsx)。行首的「→ 」「=> 」是列表的
#: 记号,不算
_DATA_LINE = re.compile(r"\S\s*(?:[==→⟶⇒]|->)|[\u2500-\u257f]|^\s*[\[{]\s*[\"'\[{]|[\"']\s*:"
r"|(?<![\w.])[A-Za-z_][\w.]*\(|(?<=[^\s.])\.[A-Za-z][A-Za-z0-9]{1,4}(?![\w.])")
def _en_sentence(line: str) -> bool:
@@ -543,41 +560,116 @@ def _concluding(line: str) -> bool:
def _written(line: str) -> bool:
"""信息串写明是文字(markdown、text 这类)、里面也看不出是代码的块里,这一行在写正文:成句、按正文的规矩会算结论,
或者一行里连着六个以上汉字。不看列表标记:标记的写法太多(- 1. 1、 一、 (1) · ●,也常不带标记),补一种漏一种;
也不当代码看,# -- 和半角引号都是正文里的符号。标题不算。"""
if _HEADING.match(line):
"""信息串写明是文字(markdown、text 这类)的块里,不是代码的这一行在写正文:成句、按正文的规矩会算结论,或者一行里
连着六个以上汉字。不看列表标记:标记的写法太多(- + 1. 1、 一、 (1) · ●,也常不带标记),补一种漏一种;也不去
字符串、行尾注释,# -- 和半角引号都是正文里的符号。整行是注释、标题的不算。"""
if _COMMENT_LINE.match(line):
return False
return _sentence(line) or _CJK_RUN.search(line) is not None or _concluding(line)
def _code_like(lines: list[str]) -> bool:
"""块里看得出是代码:有 SQL、Python 开头的行(_CODE_LINE)、YAML 的键(_YAML_KEY),或者整块是 diff(有 + 开头的
行也有 - 开头的行,其余是空格开头的上下文行、@@ 开头的块头)。"""
if any(_CODE_LINE.match(line) or _YAML_KEY.match(line) for line in lines):
return True
heads = {line[:1] for line in lines if line.strip()}
return {"+", "-"} <= heads <= {"+", "-", " ", "@"}
def _prose_line(line: str, *, code: bool) -> bool:
"""信息串为空、或者看得出是代码的块里,这一行在写正文:去掉字符串字面量和行尾注释之后成句;整块看不出是代码时,
按正文的规矩会算结论的也算(不带句读的列表项、不带标记的行)。不带句读、也不像结论的不算:YAML 里的列名、diff 里
改的口径和列表项没有分别。整行是注释的不算。"""
"""代码的行(code),或者信息串为空的块里的行,在写正文:去掉字符串字面量和行尾注释之后成句;不是代码的行另认按正文
的规矩会算结论的(不带句读的列表项、不带标记的行)。不带句读、也不像结论的不算:YAML 里的列名、diff 里改的口径和
列表项没有分别。整行是注释的不算。"""
if _COMMENT_LINE.match(line):
return False
line = _TRAILING_COMMENT.sub("", _STRING_LITERAL.sub(" ", line), count=1)
line = (_CODE_COMMENT if code else _TRAILING_COMMENT).sub("", _STRING_LITERAL.sub(" ", line), count=1)
return _sentence(line) or (not code and _concluding(line))
def _drop_block_comments(text: str) -> str:
"""去掉 /* … */(可以跨行):块注释里的中文句子也是代码的一部分。没收尾的不去,开头那一行照注释行跳过。"""
kept: list[str] = []
def _blank_spans(text: str) -> str:
"""块注释(/* … */)、三个引号括起的多行字符串涂成空格,换行留着,块里还是原来那几行:里面写着中文句子也是代码的
一部分。没收尾的不动,开头那一行照注释行跳过。"""
out: list[str] = []
at = 0
while (start := text.find("/*", at)) != -1 and (end := text.find("*/", start + 2)) != -1:
kept += [text[at:start], "\n"]
at = end + 2
return "".join(kept) + text[at:]
while (start := _SPAN_OPEN.search(text, at)) is not None:
end = text.find(_SPAN_CLOSE[start.group()], start.end())
if end == -1:
break
stop = end + len(_SPAN_CLOSE[start.group()])
out += [text[at:start.start()], "\n".join(" " * len(part) for part in text[start.start():stop].split("\n"))]
at = stop
return "".join(out) + text[at:]
def _diff(lines: list[str], *, labelled: bool) -> bool:
"""整块是 diff:有 - 开头的行也有 + 开头的行,其余是空格开头的上下文行、@@ 开头的块头。diff 里一处改动先列删掉的行、
再列加上的行,第一个改动行就是 + 的,是 + / - 开头的正负面要点(有 @@ 块头的照旧算 diff)。信息串写明是文字的块里,
+ - 后面空一格的是 Markdown 的列表项,只有紧挨着字写的(-按下单日期统计)才是 diff。"""
heads = [line[:1] for line in lines if line.strip()]
if not {"+", "-"} <= set(heads) <= {"+", "-", " ", "@"}:
return False
if labelled and any(line[:2] in ("+ ", "- ") for line in lines):
return False
return "@" in heads or next(h for h in heads if h in ("+", "-")) == "-"
def _data_like(line: str) -> bool:
"""像数据、公式的一行(_DATA_LINE),或者逗号隔开的一行表头、数据(两个以上半角逗号,逗号后面都不空格)。"""
return _DATA_LINE.search(line) is not None or (line.count(",") >= 2 and ", " not in line)
def _yaml(lines: list[str], *, labelled: bool) -> bool:
"""一段是 YAML:第一行是小写开头的键,其余每一行都是键或者缩进的行(键下面的列表项、子键),注释行不看。写明是文字的
块里另要有值为空、往下缩进的键:一行一个「键: 一句话」的(「- note: 华南销售额超过华北」)多半是正文里的标签。"""
body = [line for line in lines if line.strip() and not _COMMENT_LINE.match(line)]
if not body or not _YAML_KEY.match(body[0]):
return False
if not all(_YAML_KEY.match(line) or line[:1].isspace() for line in body[1:]):
return False
return not labelled or any(_YAML_NEST.match(line) for line in body)
def _bare_fence(line: str) -> str:
"""整行只写着围栏(``` 或 ~~~,不带信息串)的,返回这串围栏,能给块里再嵌的代码块收尾;别的行返回空串。"""
fence = _NESTED_FENCE.match(line)
return fence.group(1) if fence and not fence.group(2).strip() else ""
def _code_lines(lines: list[str], raw: list[str], *, labelled: bool) -> list[bool]:
"""逐行标出哪些是代码:块里再嵌的代码块(~~~sql),SQL、Python 的语句(从开头那一行到这一段结束,续行也算;前面的
注释行本来就不算正文),整段是 YAML 的(_yaml),像数据、公式的行(_data_like)。
lines 是涂掉块注释、多行字符串之后的行,raw 是原样的行:一段到空行为止,只写着注释的行不算空行;写明是文字的块里,
顶格的标题也另起一段。不把整块当代码:正文里另贴一段 SQL、嵌一个 ~~~sql,或者有一行像 YAML 的键,别的行照样按正文认。
再嵌的代码块要收得了才算(后面有同一种符号、不比开头短的收尾),开了没收的那一行照普通的行看。"""
code = [False] * len(lines)
paragraph: list[int] = []
fence = ""
# 每一行后面最长的收尾围栏(整行只有 ``` 或 ~~~),按符号分开记:开了收不了的围栏不算,照普通的行看,免得后面的
# 正文都被当成代码
reach = {"`": [0] * (len(lines) + 1), "~": [0] * (len(lines) + 1)}
for k in range(len(lines) - 1, -1, -1):
for after in reach.values():
after[k] = after[k + 1]
if bare := _bare_fence(lines[k]):
reach[bare[0]][k] = max(reach[bare[0]][k], len(bare))
def close() -> None:
statement = _yaml([lines[k] for k in paragraph], labelled=labelled)
for k in paragraph:
statement = statement or _CODE_LINE.match(lines[k]) is not None
code[k] = statement or _data_like(lines[k])
paragraph.clear()
for k, line in enumerate(lines):
if fence:
code[k] = True
closing = _bare_fence(line)
if closing[:1] == fence[0] and len(closing) >= len(fence):
fence = ""
continue
if (opening := _NESTED_FENCE.match(line)) and reach[opening.group(1)[0]][k + 1] >= len(opening.group(1)):
close()
fence, code[k] = opening.group(1), True
continue
if not raw[k].strip() or (labelled and _MD_HEADING.match(line)):
close()
continue
paragraph.append(k)
close()
return code
def _fenced_body(doc: dict[str, Any]) -> bool:
@@ -588,9 +680,10 @@ def _fenced_body(doc: dict[str, Any]) -> bool:
text 的原样交给 compose_doc:整段正文落在代码块里,切不出结论句,裁判没有句子可判、反方审稿记 no_claims,没挂依据
的、证据相矛盾的结论句都数出 0 句,出具就成了完整出具。有一句结论的不算:正文没被整个藏起来,结论句照常核对。
在不在写正文逐行看:信息串写明是文字、里面也看不出是代码的按 _written。信息串为空的块里也常是 SQL、Python、YAML、
diff,信息串写成 text 的也偶尔是代码,这些按 _prose_line:注释、字符串字面量里的中文句子不算;整块看得出是代码
(_code_like)的只认成句的,以 * + - 开头的 SQL 续行、中文别名里的「最高」这类词都不算。"""
在不在写正文逐行看。先标出哪些行是代码(_code_lines;信息串为空、整块是 diff 的全是代码),代码的行只认去掉注释、
字符串之后成句的(_prose_line),中文别名、YAML 的值、公式里的「最高」「占比」这类词都不算。其余的行:信息串写明
是文字的按 _written,信息串为空的块里也常是代码,去掉注释、字符串之后再看(_prose_line)。一块里只要有一行在写正文
就算,所以宁可把拿不准的行当代码:藏起来的正文总有别的行认得出,贴的一段代码、公式里却一行认错就会记错缺口。"""
from app.engine.evidence import iter_units
units = list(iter_units(doc))
@@ -603,14 +696,13 @@ def _fenced_body(doc: dict[str, Any]) -> bool:
segments = [s for s in unit.get("segments") or [] if isinstance(s, dict)]
if unit.get("see") or any(s.get("ref") for s in segments):
return True
# 围栏那两行是挂在块上的结构片段,不是块里写的内容:整块是不是 diff 要看里面每一行的开头
# 围栏那两行是挂在块上的结构片段,不是块里写的内容:算进来的话,开头那行 ```markdown 会被当成块里再嵌的代码块,
# 整块是不是 diff 也看不准
text = "".join(str(s.get("text") or "") for s in segments if s.get("kind") != "structural")
lines = _drop_block_comments(text).split("\n")
code = _code_like(lines)
if lang and not code:
if any(_written(line) for line in text.split("\n")):
return True
elif any(_prose_line(line, code=code) for line in lines):
raw, lines = text.split("\n"), _blank_spans(text).split("\n")
code = [True] * len(lines) if _diff(raw, labelled=bool(lang)) else _code_lines(lines, raw, labelled=bool(lang))
if any(_written(line) if lang and not is_code else _prose_line(line, code=is_code)
for line, is_code in zip(lines, code)):
return True
return False
+137 -20
View File
@@ -7,11 +7,16 @@ K5 让报告节点去掉整篇外层的 markdown 围栏(report._unfenced),
- 结论句策略为 judge 或 require_citation(报告节点的,或出具契约自己写的)、结论句 0 句、又有信息串为空 / markdown /
md / text / plaintext / txt / plain 的代码块,里面写着引用(片段里的引用、只挂依据的 [[see:]])或者在写正文:记一条
缺口,降档。缺口不带核查节点的前缀,界面上归在「校验未全部完成」下(run/decode.ts 的 splitIssuanceGaps)
- 在写正文:带句读的中文、成句的英文,或者按正文的规矩会算一句结论(方向词、因果词);信息串写明是文字的,一行里连着
六个以上汉字也算。列表不论用什么标记(- 1. 1、 一、 (1) · ●),也不论带不带标记,只看字
- 行为不变:有正常结论句的;代码块是 sql、python 这类代码的;信息串为空、里面是代码的(注释、字符串里的中文标点不算
句子;有 SQL / Python 开头的行、YAML 的键,或者整块是 diff 时,方向词也不算),信息串写成 text、markdown 而里面
看得出是代码的同样;信息串为空的块里不带句读、也不像结论的标签和列名;结论句策略为 off 的
- 逐行看,先标出代码的行:块里再嵌的代码块(~~~sql)、SQL / Python 的语句连同续行、整段是 YAML 的(写明是文字的块里
要有值为空的键)、像数据的行(公式、JSON、目录树、流程箭头、函数调用、文件名、逗号隔开的表头),信息串为空、整块是
diff 的全是代码。正文里另有一段代码、一行像 YAML 键的标签,不妨碍认出别的行
- 在写正文:代码的行只认去掉注释、字符串之后成句的;别的行带句读的中文、成句的英文,或者按正文的规矩会算一句结论
(方向词、因果词);信息串写明是文字的,一行里连着六个以上汉字也算。列表不论用什么标记(- + 1. 1、 一、 (1) · ●
→),也不论带不带标记,只看字
- 行为不变:有正常结论句的;代码块是 sql、python 这类代码的;写文字的块里只有代码、数据、公式的(注释、字符串里的
中文句子不算,别名、公式里的方向词也不算);信息串为空的块里不带句读、也不像结论的标签和列名;结论句策略为 off 的
- 取舍:写成 text、markdown 的 diff,+ - 后面又空一格的,和列表分不开,照列表看;信息串为空的块里先列 - 再列 + 的
要点、整段都是「小写键: 值」的,和 diff、YAML 分不开,照代码看
模型一律是 MockChatModel 剧本,数据源是现造的 SQLite 文件,区域、金额都是虚构的。
"""
@@ -62,6 +67,15 @@ def listed(first: str, second: str) -> str:
return f"## 本周结论\n\n{first}{LINES[0]}\n{second}{LINES[1]}"
#: + / - 开头的正负面要点:不包代码块时两句都是结论(「增长」「回落」)
PROS_CONS = "+ 华东销售额增长明显\n- 华北销售额回落"
#: 正文后面另贴一段取数的 SQL:块里再嵌一个 ~~~sql 代码块,或者缩进四格
WITH_TILDE = f"{BARE_LIST}\n\n## 取数\n\n~~~sql\nSELECT region, SUM(amount) AS gmv FROM orders GROUP BY region\n~~~"
WITH_INDENTED = f"{BARE_LIST}\n\n## 取数\n\n select region, sum(amount) as gmv from orders group by region"
#: 列表项前面写着小写的英文标签,看着像 YAML 的键
WITH_LABEL = "## 本周结论\n\n- 华东销售额居各区域之首\n- note: 华南销售额超过华北"
HIDDEN = [pytest.param(PREAMBLE, id="preamble"), pytest.param(UNCLOSED, id="unclosed"),
pytest.param(AS_TEXT, id="text"), pytest.param(fenced(TEXT, "plaintext"), id="plaintext"),
pytest.param(f"以下是本周周报:\n\n{fenced(SEE_LIST)}", id="see-only-list"),
@@ -73,7 +87,15 @@ HIDDEN = [pytest.param(PREAMBLE, id="preamble"), pytest.param(UNCLOSED, id="uncl
pytest.param(f"以下是本周周报:\n\n{fenced(listed('● ', '● '))}", id="black-circle"),
pytest.param(f"以下是本周周报:\n\n{fenced(UNMARKED)}", id="lines-without-markers"),
# 信息串为空:第二句按正文的规矩是一句结论
pytest.param(f"以下是本周周报:\n\n{fenced(listed('1、', '2、'), '')}", id="enumerated-no-info")]
pytest.param(f"以下是本周周报:\n\n{fenced(listed('1、', '2、'), '')}", id="enumerated-no-info"),
# + / - 开头的正负面要点:写明是文字的块里是列表;信息串为空的块里先列 + 的也不是 diff
pytest.param(f"以下是本周周报:\n\n{fenced(PROS_CONS)}", id="pros-and-cons"),
pytest.param(f"以下是本周周报:\n\n{fenced(PROS_CONS, 'text')}", id="pros-and-cons-text"),
pytest.param(f"以下是本周周报:\n\n{fenced(PROS_CONS, '')}", id="pros-and-cons-no-info"),
# 正文里另有几行代码、像 YAML 键的标签:只有那几行当代码看,正文照样认得出
pytest.param(f"以下是本周周报:\n\n{fenced(WITH_TILDE)}", id="tilde-block-inside"),
pytest.param(f"以下是本周周报:\n\n{fenced(WITH_INDENTED)}", id="indented-sql-inside"),
pytest.param(f"以下是本周周报:\n\n{fenced(WITH_LABEL)}", id="lowercase-label")]
def doc_of(raw: str) -> dict:
@@ -130,6 +152,43 @@ def doc_of(raw: str) -> dict:
pytest.param("```\n华北销售额回落\n```", id="no-info-conclusion"),
# 首字母大写的「Note:」是正文里的标签,不是 YAML 的键
pytest.param("```markdown\nNote: 以下为初步结论\n- 华南销售额超过华北\n```", id="capitalised-label"),
# + / - 开头的正负面要点:写明是文字的块里是列表,不论哪个在前;信息串为空的块里先列 + 的不是 diff(diff 里一处
# 改动先列删掉的行),+ 下面缩进的子项也一样
pytest.param(f"```markdown\n{PROS_CONS}\n```", id="plus-minus-markdown"),
pytest.param("```text\n- 华北销售额回落\n+ 华东销售额增长明显\n```", id="minus-plus-text"),
# 取舍:写成 text、markdown 的 diff,+ - 后面又空一格,和列表分不开,照列表看(diff 该写 diff 信息串)
pytest.param("```text\n- 按下单日期统计\n+ 按支付日期统计\n```", id="spaced-diff-labelled-text"),
pytest.param(f"```\n{PROS_CONS}\n```", id="plus-minus-no-info"),
pytest.param("```\n+ 华东销售额增长明显\n - 主要来自新店\n- 华北销售额回落\n```", id="plus-minus-nested-no-info"),
# 正文里另有几行代码:块里再嵌的 ~~~sql、缩进的 SQL 只是那几行,不把整块都当代码
pytest.param(f"```markdown\n{WITH_TILDE}\n```", id="tilde-block-inside"),
pytest.param(f"```\n{WITH_TILDE}\n```", id="tilde-block-inside-no-info"),
pytest.param(f"```markdown\n{WITH_INDENTED}\n```", id="indented-sql-inside"),
pytest.param(f"```\n{WITH_INDENTED}\n```", id="indented-sql-inside-no-info"),
pytest.param("```text\n- 华南销售额超过华北\n\nselect region from orders\n```", id="sql-in-another-paragraph"),
pytest.param("```markdown\n~~~sql\nSELECT region FROM orders\n~~~\n\n- 华南销售额超过华北\n```",
id="prose-after-nested-block"),
# 块里再嵌的代码块开了没收:不当围栏,后面的正文照样认
pytest.param("```markdown\n## 取数\n\n~~~sql\nSELECT region FROM orders\n\n## 本周结论\n\n- 华南销售额超过华北\n```",
id="nested-block-left-open"),
pytest.param("```markdown\n~~~~sql\nSELECT region FROM orders\n~~~\n\n- 华南销售额超过华北\n```", id="nested-close-too-short"),
# 收尾的围栏比开头长也收得了
pytest.param("```markdown\n~~~~sql\nSELECT region FROM orders\n~~~~~\n\n- 华南销售额超过华北\n```", id="nested-close-longer"),
# 写明是文字的块里,顶格的标题另起一段:上一段的 SQL 管不到标题下面的列表
pytest.param("```markdown\n## 取数\nselect region from orders\n## 本周结论\n- 华南销售额超过华北\n```", id="heading-ends-sql"),
# 行首的箭头是列表的记号,不是连起流程的箭头
pytest.param("```markdown\n→ 华东销售额居各区域之首\n→ 华南销售额超过华北\n```", id="arrow-bullets"),
# 小写英文键开头的行:一段里有不缩进、又不是键的行,不是 YAML;写明是文字的块里,没有值为空的键(下面缩进着
# 列表、子键)的「键: 一句话」也是正文里的标签
pytest.param(f"```markdown\n{WITH_LABEL}\n```", id="lowercase-label-after"),
pytest.param("```\n- note: 华南销售额超过华北\n- 华东销售额居各区域之首\n```", id="lowercase-label-first-no-info"),
pytest.param("```text\n- top: 华东销售额居各区域之首\n- note: 华南销售额超过华北\n```", id="lowercase-labels-only"),
pytest.param("```markdown\nsummary: 华南销售额超过华北\n```", id="key-and-conclusion"),
# 首字母大写的英文词开头是正文里的英文,不是 SQL 的关键字
pytest.param("```markdown\nFrom 数据看 华南销售额超过华北\n```", id="capitalised-keyword"),
pytest.param("```\nUpdate 华南销售额超过华北\n```", id="capitalised-keyword-no-info"),
# def、class 不写成定义的样子,是正文里的词(A 类门店)
pytest.param("```markdown\nclass A 门店销售额超过 class B 门店\n```", id="class-of-stores"),
])
def test_a_report_body_in_a_prose_code_block_is_recognised(raw):
doc = doc_of(raw)
@@ -174,12 +233,52 @@ def test_a_report_body_in_a_prose_code_block_is_recognised(raw):
pytest.param("```\nSELECT MAX(amount) AS 最高金额, MIN(amount) AS 最低金额 FROM orders\n```", id="sql-aliases"),
pytest.param("```\nmetrics:\n - 退款占比\n - 销售额增长率\n```", id="yaml-with-conclusion-words"),
pytest.param("```\n- name: 退款占比\n unit: 百分比\n```", id="yaml-mappings"),
# 取舍:信息串为空、整段都是「小写键: 值」的照 YAML 看,值里写着结论也一样(写明是文字的块里照正文看)
pytest.param("```\n- top: 华东销售额居各区域之首\n- note: 华南销售额超过华北\n```", id="all-keys-no-info"),
pytest.param("```\n- 退款率下降\n+ 退款率上升\n```", id="diff-with-conclusion-words"),
pytest.param("```\n@@ -1,2 +1,2 @@\n 口径说明\n-销售额按下单日期统计后下降\n+销售额按支付日期统计后上升\n```", id="diff-hunk"),
# 写成 text、markdown 的 diff:+ - 紧挨着字写,不是 Markdown 的列表项
pytest.param("```text\n-退款率下降时提醒\n+退款率上升时提醒\n```", id="diff-labelled-text"),
pytest.param("```markdown\n@@ -1 +1 @@\n-销售额按下单日期统计后下降\n+销售额按支付日期统计后上升\n```",
id="diff-hunk-labelled-markdown"),
# 有 @@ 块头的,先列加上的行也是 diff
pytest.param("```\n@@ -1,2 +1,2 @@\n+销售额按支付日期统计后上升\n-销售额按下单日期统计后下降\n```", id="diff-hunk-plus-first"),
# 代码的行里,-- 紧挨着前面的字也是注释
pytest.param("```\nSELECT SUM(amount)--按区域汇总,排除退款\nFROM orders\n```", id="sql-comment-without-space"),
# 信息串写成 text、markdown,里面却看得出是代码:照代码看,注释里的句子、别名里的方向词不算
pytest.param("```text\nSELECT region AS 区域, MAX(amount) AS 最高金额 FROM orders -- 按区域汇总,排除退款\n```",
id="sql-labelled-text"),
pytest.param("```markdown\nmetrics:\n - 退款占比\n - 销售额增长率\n```", id="yaml-labelled-markdown"),
pytest.param("```text\n# 指标\nmetrics:\n # 核心指标\n - 退款占比\n```", id="yaml-with-comments"),
pytest.param("```\n# 核心指标\nmetrics:\n - 退款占比\n```", id="yaml-with-comments-no-info"),
# 写成 text 的 SQL 跨好几行:以语句开头的一段连同续行都是代码,开头的注释行也是;块注释、多行字符串整段不算,
# 只写着块注释的一行也不把一段断开
pytest.param("```text\nSELECT region,\n amount AS 销售额增长率\n FROM orders\n```", id="sql-continuation-text"),
pytest.param("```text\nSELECT region,\n/* 别名 */\n amount AS 销售额增长率\n FROM orders\n```",
id="sql-with-block-comment-text"),
pytest.param("```text\n-- 按区域汇总,排除退款。\nSELECT region, amount AS 最高金额 FROM orders\n```", id="sql-leading-comment"),
pytest.param('```\ndef summary():\n """\n 按支付日统计,排除退款。\n """\n return 1\n```', id="docstring"),
# 像数据、公式的行(等号、JSON、画目录树的制表符、箭头连起的流程、函数调用、文件名、逗号隔开的表头)照代码看:
# 去掉字符串之后不成句的不算正文,结论词、六字连写只看写正文的行
pytest.param("```\n退款占比 = 退款金额 / 销售额\n```", id="formula-no-info"),
pytest.param("```text\n销售额增长率 = (本期销售额 - 上期销售额) / 上期销售额\n```", id="formula-text"),
pytest.param('```text\n{"区域": "华东", "说明": "按支付日期统计"}\n```', id="json-text"),
pytest.param('```markdown\n "说明": "按支付日期统计,排除退款",\n```', id="json-member"),
pytest.param('```text\n["订单编号", "支付日期", "销售额增长率"]\n```', id="json-array"),
pytest.param("```text\n报表/\n├── 华东门店销售周报.xlsx\n└── 华南门店销售周报.xlsx\n```", id="tree-text"),
pytest.param("```text\n周报\n├── 华东门店销售周报\n│ └── 第四周销售明细\n└── 华南门店销售周报\n```", id="tree-of-folders"),
pytest.param("```text\n上传表格导入 → 字段映射校验 → 生成周报\n```", id="flow-text"),
pytest.param("```text\nprint('导入完成,请查收')\n```", id="call-with-string-text"),
pytest.param("```markdown\nrows.groupby(区域).agg(销售额增长率)\n```", id="call-text"),
pytest.param("```markdown\n华东门店销售周报.xlsx\n华南门店销售周报.xlsx\n```", id="file-names"),
pytest.param("```text\n订单编号,支付日期,销售额增长率\n```", id="csv-header"),
# 块里再嵌的代码块照代码看:注释里的句子、别名里的结论词、列着指标名的 YAML 都不算
pytest.param("```markdown\n## 取数\n\n~~~sql\nSELECT MAX(amount) AS 最高金额 FROM orders -- 按区域汇总,排除退款\n~~~\n```",
id="tilde-block-only"),
pytest.param("```markdown\n## 口径\n\n~~~yaml\n- 退款占比\n- 销售额增长率\n~~~\n```", id="nested-yaml-block"),
pytest.param("```markdown\n~~~~\n- 退款占比\n- 销售额增长率\n~~~~~\n```", id="nested-yaml-longer-close"),
# 开了没收的那一行照普通的行看,里面的 SQL 照样认得出
pytest.param("```markdown\n## 取数\n\n~~~sql\nSELECT MAX(amount) AS 最高金额 FROM orders\n```", id="nested-sql-left-open"),
# 有正常的结论句:正文没被藏起来
pytest.param("华东销售额超过华南。\n\n```markdown\n华东销售额 1500,居各区域之首。\n```", id="claims-outside"),
# 没有代码块
@@ -189,14 +288,27 @@ def test_code_or_a_report_with_claims_is_not_taken_for_a_hidden_body(raw):
assert _fenced_body(doc_of(raw)) is False
def test_a_long_line_without_a_full_stop_takes_linear_time():
"""很长、不带句末标点的一行英文单词:逐串数单词,耗时随行长线性增长。按正则回溯认英文句子时,四万多字的一行要十几
秒(行长翻倍、耗时翻四倍),出口复核在事件循环上同步执行,整个服务都会卡住。"""
line = "alpha beta " * 4000
doc = {"blocks": [{"type": "code", "lang": "", "units": [
@pytest.mark.parametrize("lang", ["", "markdown"])
@pytest.mark.parametrize("line", [
pytest.param("alpha beta " * 4000, id="words"),
# 逐行认代码、认数据的几条规则:一长串字母后面没有括号、冒号,一长串空格、横线,一长串汉字、逗号、引号
pytest.param("a" * 40000, id="one-long-word"),
pytest.param(" " * 20000 + "-" + " " * 20000 + "a", id="spaces-and-a-dash"),
pytest.param("华" * 40000, id="cjk"),
pytest.param("a," * 20000, id="commas"),
pytest.param(' "a' * 13000, id="open-quotes"),
pytest.param('"""' * 13000, id="triple-quotes"),
# 好多行围栏:收得了的、开了没收的
pytest.param("~~~\n" * 13000, id="bare-fences"),
pytest.param("~~~sql\n" * 13000, id="fences-left-open"),
])
def test_a_long_line_without_a_full_stop_takes_linear_time(lang, line):
"""很长、不带句末标点的一行:逐串数单词、逐行认代码,耗时随行长线性增长。按正则回溯认英文句子时,四万多字的一行要
十几秒(行长翻倍、耗时翻四倍),出口复核在事件循环上同步执行,整个服务都会卡住。"""
doc = {"blocks": [{"type": "code", "lang": lang, "units": [
{"kind": "code", "see": [], "segments": [{"kind": "text", "text": line}]}]}]}
started = time.perf_counter()
assert _fenced_body(doc) is False
_fenced_body(doc)
assert time.perf_counter() - started < 1.0
@@ -283,16 +395,21 @@ async def test_require_citation_records_the_gap_too(monkeypatch, claims, contrac
assert issuance["claims"]["uncited_claims"] == 0
@pytest.mark.parametrize("code", [
pytest.param("SELECT region, SUM(amount) AS gmv FROM orders GROUP BY region -- 按区域汇总,排除退款",
@pytest.mark.parametrize("info, code", [
pytest.param("", "SELECT region, SUM(amount) AS gmv FROM orders GROUP BY region -- 按区域汇总,排除退款",
id="sql-trailing-comment"),
pytest.param("columns:\n - 订单编号\n - 支付日期", id="yaml-list"),
pytest.param("- 按下单日期统计\n+ 按支付日期统计", id="diff"),
pytest.param("", "columns:\n - 订单编号\n - 支付日期", id="yaml-list"),
pytest.param("", "- 按下单日期统计\n+ 按支付日期统计", id="diff"),
pytest.param("", "退款占比 = 退款金额 / 销售额", id="formula"),
pytest.param("text", "销售额增长率 = (本期销售额 - 上期销售额) / 上期销售额", id="formula-text"),
pytest.param("text", '{"区域": "华东", "说明": "按支付日期统计"}', id="json-text"),
pytest.param("text", "报表/\n├── 华东门店销售周报.xlsx\n└── 华南门店销售周报.xlsx", id="tree-text"),
])
async def test_code_in_a_report_without_claims_is_issued_as_before(monkeypatch, code):
"""一句结论都没有的报告(只有口径说明)贴了一段不带信息串的代码:行尾注释里写着中文句子的 SQL、列着中文列名的
YAML、改口径的 diff。那是代码,不是藏起来的正文,照常完整出具,不记这条缺口。"""
writer(monkeypatch, f"## 口径说明\n\n本周报告按支付日期统计。\n\n```\n{code}\n```")
async def test_code_in_a_report_without_claims_is_issued_as_before(monkeypatch, info, code):
"""一句结论都没有的报告(只有口径说明)贴了一段代码、公式或数据:行尾注释里写着中文句子的 SQL、列着中文列名的
YAML、改口径的 diff(这三种不带信息串),带「占比」「增长率」的公式,写成 text 的 JSON、目录树。那不是藏起来的
正文,照常完整出具,不记这条缺口。"""
writer(monkeypatch, f"## 口径说明\n\n本周报告按支付日期统计。\n\n```{info}\n{code}\n```")
row = await finish(weekly(claims="require_citation"))
assert row.status == "succeeded", row.error
issuance = row.output["_issuance"]