{esc(item["label"])}
{item["bytes"]:,} 字节 · SHA-256:{esc(item["sha256"])}
{esc(_diagnostic_log_excerpt(item["text"]))}'
+ f'From d50ad9e282fefb8990d3a1a8f92ce25dd7067224 Mon Sep 17 00:00:00 2001
From: milky886 SHA-256: 本次没有登记重试;只保留一个物理调用。 {item["bytes"]:,} 字节 · SHA-256: 没有可展示的逐行日志。 下载{esc(item["label"])}完整文件 · '
+ f'{item["bytes"]:,} 字节 · SHA-256: 没有可下载的逐行日志文件。 本次没有登记重试;只保留一个物理调用。 日志可用性:{esc(availability)} 最后报错:{esc(error_type)} · {esc(error_message)} {output_link} {esc(private_note)} 没有可供人工定位的连续文字样例;机器覆盖率仍按上方完整实际返回计算。 这一段标准答案产生 {expected_count:,} 个连续六字符片段;在上方完整实际返回({len(output):,} 字符)中命中 {matched:,} 个,所以覆盖率为 {matched:,} ÷ {expected_count:,} = {pct(actual["coverage"])}。目标 {pct(actual["minimum"])},硬下限 {pct(actual.get("floor",actual["minimum"]))}。 这是从完整实际返回中选出的一小段,便于确认文字确实出现;它不是覆盖率计算所使用的全部实际输出。 完整实际返回中没有找到属于这一段的可读对应文字。 本段覆盖率为 {pct(actual["coverage"])}。每段至少达到 {pct(actual.get("floor",actual["minimum"]))};达到 {pct(actual["minimum"])} 时算作主要完整段落。 共 {len(anchors)} 组稳定文字,全部属于标准答案;当前区域显示约 5 组高度,向下滚动查看全部。 {esc(excerpt)} 实际返回中未找到可供定位的连续原文。 {esc(expected)} 共 {len(cards)} 项稳定文字判定,全部参与评分;当前区域显示约 5 项高度,向下滚动查看全部。 本例未设置稳定文字核对点,不作文字内容已核验的结论。 未通过原因:'+esc(stable_failure_summary(m))+'。 未通过原因:'+esc(stable_failure_summary(m))+'。 查看判定使用的标准答案'
'查看本次完整实际返回'
'查看逐项判定'
- '{esc(readable_output(output))}'
- else:
- output_html=f'原生事件日志
'+''.join(
- f'{esc(item["label"])}
{esc(item["sha256"])}{esc(item["text"])}{esc(json.dumps(public_provider_metadata(retry),ensure_ascii=False,indent=2))}')
- private_note=('原始供应商响应信封仅在内部证据中保留,不进入公开弹窗或发布包。'
- if metadata.get('raw_response_path') or metadata.get('private_artifacts') else
- '没有登记仅限内部保存的原始响应信封。')
+ retry_count=(len(retry.get('attempts',[])) if isinstance(retry,dict) else record.get('physical_attempt_count') or 1)
+ artifact=contained_path(out,record['artifact_path'])
+ output_bytes=artifact.stat().st_size if artifact.is_file() else len(output.encode('utf-8'))
+ output_sha=hashlib.sha256(artifact.read_bytes()).hexdigest() if artifact.is_file() else hashlib.sha256(output.encode('utf-8')).hexdigest()
facts=[('请求路线',route),('请求状态',status),('总耗时',ms(record.get('elapsed_ms'))),
('开始时间',record.get('started_at') or '—(未记录)'),('完成时间',record.get('completed_at') or '—(未记录)'),
- ('物理调用',str(retry_count))]
- fact_html=''.join(f'{esc(item["label"])}
{esc(item["sha256"])}{esc(_diagnostic_log_excerpt(item["text"]))}'
+ f'{esc(item["sha256"])}{esc(json.dumps(public_provider_metadata(retry),ensure_ascii=False,indent=2))}')
+ output_link=(f'下载完整实际输出 · '
+ f'{output_bytes:,} 字节 · SHA-256:{esc(output_sha)}')
+ private_note=('原始供应商响应信封仅在内部证据中保留,不进入公开报告或发布包。'
+ if metadata.get('raw_response_path') or metadata.get('private_artifacts') else
+ '没有登记仅限内部保存的原始响应信封。')
+ return (f'运行日志与证据
'
+ f'{fact_html}
'
+ f'查看错误与关键日志
查看并下载完整 stdout / stderr / debug 证据
查看重试与物理调用
查看公开适配器字段与证据边界
{esc(json.dumps(public_metadata,ensure_ascii=False,indent=2))}{head} {body}{esc(excerpt)}' if excerpt else
- '{index}. {esc(label)}
{esc(expected)}{esc(passage)}' if passage else
+ '{index}. {esc(label)}
{esc(expected)}{esc(count)}
{esc(count)}
稳定文字 {index}
{badge}
{esc(json.dumps(public_provider_metadata(r.get("provider_metadata",{})),ensure_ascii=False,indent=2))}{link(c["url"],"打开源页 ↗")}{link("../datasets/"+c["id"]+".html","查看该任务的全部方法")}
') body+=section('运行与评分信息',runtime,'run-info') - body+=call_log_dialog(out,r,c,output) + body+=run_log_section(out,r,output) if r.get('provider_metadata',{}).get('rendered_html_path'): body+=section('浏览器 HTML 证据',''+link('../'+r['provider_metadata']['rendered_html_path'],'下载转换前的完整 DOM HTML')+'
') if r.get('provider_metadata',{}).get('raw_stdout_path'): diff --git a/harness/web_fetch/report_i18n.py b/harness/web_fetch/report_i18n.py index 256f421fb..5a6d00826 100644 --- a/harness/web_fetch/report_i18n.py +++ b/harness/web_fetch/report_i18n.py @@ -2951,6 +2951,7 @@ def _register_release_translations() -> None: "这次历史运行没有保存统一事件日志。下面由最终输出、结果字段和公开适配器元数据合成,不能还原未记录的逐行过程。": "This historical run did not retain a unified event log. The view below is reconstructed from the final output, result fields, and public adapter metadata; it cannot recover unrecorded step-by-step events.", "最后输出": "Final output", "最后报错": "Final error", + "最后报错:": "Final error: ", "无报错;请求记录为成功。": "No error was recorded; the request is recorded as successful.", "请求失败,但历史记录未保存错误文字。": "The request failed, but the historical record retained no error text.", "请求路线": "Request route", @@ -2971,8 +2972,67 @@ def _register_release_translations() -> None: "最终输出确实为空;本次历史记录没有保存更具体的原因,不能据此推断是服务端、网络还是适配器问题。": "The final output is genuinely empty. This historical record retained no more specific reason, so it cannot establish whether the cause was the server, network, or adapter.", "—(未记录)": "— (not recorded)", "—(未记录错误类型)": "— (error type not recorded)", + "查看运行日志与证据": "View run logs and evidence", + "运行日志与证据": "Run logs and evidence", + "这次历史调用没有保存逐行 debug 日志;不会补造不存在的日志。": "This historical call did not retain a line-by-line debug log; no missing log is reconstructed.", + "运行记录声明了日志文件,但公开证据中没有可读取文件;日志证据不完整。": "The run record declares a log file, but no readable file is present in the public evidence; log evidence is incomplete.", + "运行日志": "Run logs", + "查看错误与关键日志": "View errors and key log lines", + "没有可展示的逐行日志。": "No line-by-line log is available to display.", + "查看并下载完整 stdout / stderr / debug 证据": "View and download complete stdout / stderr / debug evidence", + "下载完整实际输出": "Download complete actual output", + "没有可下载的逐行日志文件。": "No line-by-line log file is available to download.", + "查看重试与物理调用": "View retries and physical calls", + "查看公开适配器字段与证据边界": "View public adapter fields and evidence boundaries", + "原始供应商响应信封仅在内部证据中保留,不进入公开报告或发布包。": "The original provider response envelope is retained only in internal evidence and is excluded from the public report and publication bundle.", + "这一位置的完整标准正文": "Complete reference text for this position", + "完整实际返回中的对应文字": "Corresponding text in the complete actual output", + "完整实际返回中没有找到属于这一段的可读对应文字。": "No readable corresponding text for this region was found in the complete actual output.", + "查看各段完整标准正文和实际对应文字": "View complete reference text and corresponding actual text for each region", + "正文判定:": "Body-text rule: ", }) +REGEX.insert(0, ( + re.compile(r"^(\d+) 份真实日志文件已保存并通过 SHA-256 校验。$"), + r"\1 real log files were retained and verified by SHA-256.", +)) +REGEX.insert(0, ( + re.compile(r"^([\d,]+) 字节 · SHA-256:$"), + r"\1 bytes · SHA-256: ", +)) +REGEX.insert(0, ( + re.compile(r"^· ([\d,]+) 字节 · SHA-256:$"), + r"· \1 bytes · SHA-256: ", +)) +REGEX.insert(0, ( + re.compile(r"^(\d+) 份真实日志文件可以读取;其中至少一份没有在运行记录中声明 SHA-256。$"), + r"\1 real log files are readable; at least one has no SHA-256 declared in the run record.", +)) +REGEX.insert(0, ( + re.compile(r"^下载(.+)完整文件$"), + r"Download the complete \1 file", +)) +REGEX.insert(0, ( + re.compile(r"^正文判定:本例分为 (\d+) 段。每段至少达到 ([\d.]+)%,其中至少 (\d+) 段达到 ([\d.]+)%。先看下表;需要核对原文时,再展开下面的左右对照。$"), + r"Body-text rule: this case has \1 regions. Every region must reach at least \2%, and at least \3 regions must reach \4%. Check the table first, then expand the side-by-side evidence when needed.", +)) +REGEX.insert(0, ( + re.compile(r"^本例分为 (\d+) 段。每段至少达到 ([\d.]+)%,其中至少 (\d+) 段达到 ([\d.]+)%。先看下表;需要核对原文时,再展开下面的左右对照。$"), + r"This case has \1 regions. Every region must reach at least \2%, and at least \3 regions must reach \4%. Check the table first, then expand the side-by-side evidence when needed.", +)) +REGEX.insert(0, ( + re.compile(r"^本段覆盖率为 ([\d.]+)%。每段至少达到 ([\d.]+)%;达到 ([\d.]+)% 时算作主要完整段落。$"), + r"This region has \1% coverage. Every region must reach at least \2%; reaching \3% counts as a substantially complete region.", +)) +REGEX.insert(0, ( + re.compile(r"^共 (\d+) 组稳定文字,全部属于标准答案;当前区域显示约 5 组高度,向下滚动查看全部。$"), + r"All \1 stable-text groups belong to the reference answer. This area shows about five groups at a time; scroll to view all.", +)) +REGEX.insert(0, ( + re.compile(r"^共 (\d+) 项稳定文字判定,全部参与评分;当前区域显示约 5 项高度,向下滚动查看全部。$"), + r"All \1 stable-text checks contribute to scoring. This area shows about five at a time; scroll to view all.", +)) + REGEX.insert(0, ( re.compile(r"^(\d+) 个物理调用日志已保存并通过 SHA-256 校验。$"), r"Logs for \1 physical calls were retained and verified by SHA-256.", diff --git a/tests/test_end_to_end.py b/tests/test_end_to_end.py index a6a3673ba..0a0e93adf 100644 --- a/tests/test_end_to_end.py +++ b/tests/test_end_to_end.py @@ -31,7 +31,9 @@ def test_fixture_run_and_current_report(self): self.assertIn(records[1]['run_id'],analysis) self.assertIn('开始时间(UTC)',attempt) self.assertGreaterEqual(attempt.count('查看运行日志与证据',attempt)
+ self.assertIn('',attempt)
+ self.assertNotIn('data-call-log-open="call-log-dialog"',attempt)
for kind in ('reference', 'output', 'metadata'):
self.assertIn(f'', attempt)
self.assertTrue((report.parent/'review/index.html').exists())
diff --git a/tests/test_report_body_scope.py b/tests/test_report_body_scope.py
index 29e1b86c5..95675a226 100644
--- a/tests/test_report_body_scope.py
+++ b/tests/test_report_body_scope.py
@@ -10,7 +10,7 @@
from harness.web_fetch.report import (
TRACKS, attempt_row, case_rate, generate, overview_card, ranking, reason, stable_anchor_evidence,
- stable_failure_summary, write_support,
+ stable_answer_panel, stable_checks, stable_failure_summary, write_support,
)
from harness.web_fetch.report_components import (
task_contract_guide, task_pass_guide, examples, list_rule_description, metric_guide, reference_status, task_instruction,
@@ -49,6 +49,21 @@ def table_cells(rendered, identifier):
class ReportBodyScopeTests(unittest.TestCase):
+ def test_long_stable_text_and_record_lists_scroll_only_after_five_items(self):
+ anchors=[{'id':f'a-{index}','alternatives':[f'Stable text {index}']} for index in range(6)]
+ contract={'minimum_records':6,'maximum_records':6,'stable_text_anchors':anchors}
+ answer=stable_answer_panel(contract)
+ self.assertIn('',answer)
+ metrics={'stable_anchor_results':[
+ {'id':item['id'],'alternatives':item['alternatives'],'matched':item['alternatives'],'pass':True}
+ for item in anchors],
+ 'stable_records':[{'fields':{'title':f'Item {index}'},'valid':True,'errors':[]} for index in range(6)],
+ 'stable_page_pattern_results':[],'stable_task_errors':[]}
+ evidence=stable_anchor_evidence(metrics,'\n'.join(item['alternatives'][0] for item in anchors))
+ self.assertIn('',evidence)
+ self.assertIn('',stable_checks(metrics))
+ self.assertNotIn('data-scroll-after="5"',stable_answer_panel({**contract,'stable_text_anchors':anchors[:5]}))
+
def test_request_failure_reason_precedes_derived_empty_body_metrics(self):
c=case('document','body','document');r=record(c,passed=False,ok=False)
r['metrics']['body_region_results']=[{'coverage':0,'target_pass':False}]
@@ -285,18 +300,25 @@ def test_body_detail_leads_with_exact_reference_and_output_and_explains_position
out=Path(directory);artifact=out/r['artifact_path'];artifact.parent.mkdir(parents=True)
artifact.write_text('alpha beta gamma delta')
with patch('harness.web_fetch.report_explainer.render_explainer',return_value='article'), patch('harness.web_fetch.report.write_support'):
- generate(out,{'cases':[c]},[r],[])
+ generate(out,{'cases':[c]},[r],[],include_english=True)
detail=(out/'attempts'/f"{r['attempt_id']}.html").read_text()
self.assertLess(detail.index('标准答案与实际返回'),detail.index('为什么这样判'))
self.assertIn('alpha beta gamma delta',detail)
- for phrase in ['评分使用上方完整标准答案和完整实际返回', '连续六字符片段',
- '12 ÷ 12 = 100.0%', '定位样例(仅供人工查看)',
+ for phrase in ['正文判定:', '每段至少达到 60.0%',
+ '本段覆盖率为 100.0%', '完整实际返回中的对应文字',
+ '查看各段完整标准正文和实际对应文字',
'查看判定使用的标准答案', '查看本次完整实际返回']:
self.assertIn(phrase,detail)
- self.assertNotIn('实际返回中的对应原文',detail)
+ self.assertNotIn('定位样例(仅供人工查看)',detail)
+ self.assertNotIn('这项分数怎样计算',detail)
+ self.assertEqual(detail.count('data-kind="region-output"'),1)
self.assertIn('方法直接取得原始文档 → pypdf → 可读正文',detail)
self.assertIn('标准答案位置',detail)
self.assertNotIn('第 1/1 段',detail)
+ english=(out/'en/attempts'/f"{r['attempt_id']}.html").read_text()
+ for phrase in ['Body-text rule:', 'Every region must reach at least 60.0%',
+ 'Corresponding text in the complete actual output']:
+ self.assertIn(phrase,english)
dataset=(out/'datasets/article.html').read_text()
self.assertIn('同一份标准答案',dataset)
self.assertNotIn('字符六元组数',dataset)
@@ -315,16 +337,17 @@ def test_attempt_call_record_explains_empty_output_and_hides_private_envelope(se
generate(out,{'cases':[c]},[r],[],include_english=True)
detail=(out/'attempts'/f"{r['attempt_id']}.html").read_text()
english=(out/'en/attempts'/f"{r['attempt_id']}.html").read_text()
- for phrase in ['data-call-log-open="call-log-dialog"','')
def pct(n):return '—' if n is None else f'{n*100:.1f}%'
def ms(n):return '—' if n is None else f'{n:,.0f} ms'
def link(url,label):return f'{esc(label)}'
@@ -906,11 +892,9 @@ def _best_evidence_excerpt(expected, output):
if score>best_score:best,best_score=chunk,score
return best if best_score>=min(.25,3/max(1,sum(wanted.values()))) else ''
-def _complete_evidence_lines(expected, output):
- """Return complete readable output lines that materially match one rule region."""
- from .body_coverage import tokens
+def _exact_evidence_lines(expected, output):
+ """Return complete output lines containing the normalized expected text."""
from .metrics import normalize_for_match
- wanted=Counter(tokens(expected))
normalized_expected=normalize_for_match(expected)
selected=[]
for raw in output.splitlines():
@@ -919,11 +903,6 @@ def _complete_evidence_lines(expected, output):
normalized_line=normalize_for_match(line)
if normalized_expected and normalized_expected in normalized_line:
if line not in selected:selected.append(line)
- continue
- found=Counter(tokens(line));denominator=sum(wanted.values())
- overlap=sum((wanted & found).values())
- if wanted and (overlap/max(1,denominator)>=.18 or overlap/max(1,sum(found.values()))>=.45):
- if line not in selected:selected.append(line)
return '\n\n'.join(selected)
def _diagnostic_log_excerpt(text):
@@ -936,6 +915,68 @@ def _diagnostic_log_excerpt(text):
if not selected:selected.update(range(min(12,len(lines))))
return '\n'.join(f'{index+1:>6} {lines[index]}' for index in sorted(selected))
+def _call_log_excerpt(item):
+ """Prefer exact structured diagnostics while retaining plain legacy logs."""
+ payload=item['payload']
+ if not payload:return _diagnostic_log_excerpt(item['text'])
+ parts=[]
+ error=payload.get('error') if isinstance(payload.get('error'),dict) else {}
+ process=payload.get('process') if isinstance(payload.get('process'),dict) else {}
+ events=payload.get('events') if isinstance(payload.get('events'),list) else []
+ if error.get('type') or error.get('message'):
+ parts.append('error: '+str(error.get('type') or '—')+' · '+str(error.get('message') or '—'))
+ if process.get('stderr'):
+ parts.append('stderr:\n'+str(process['stderr']))
+ elif process.get('stdout'):
+ parts.append('stdout:\n'+str(process['stdout']))
+ for event in events:
+ if isinstance(event,dict) and (event.get('error_type') or event.get('stage')):
+ parts.append('event: '+json.dumps(event,ensure_ascii=False,sort_keys=True))
+ return _diagnostic_log_excerpt('\n'.join(parts) if parts else item['text'])
+
+def _body_region_evidence(content,contract):
+ """Map canonical scorer allocations to complete lines without changing scoring output."""
+ from math import ceil
+ from .body_coverage import _global_region_allocation,compact_ngrams,tokens
+ version=contract['version'];size=6 if version==3 else 3
+ units=tokens(content) if version==1 else compact_ngrams(content,size)
+ predicted=Counter(units);expected={}
+ for region in contract['regions']:
+ values=(region['text'].split() if version==1 else compact_ngrams(region['text'],size))
+ expected[region['id']]=Counter(values)
+ if version==3:
+ floor=contract['minimum_floor'];target=contract['minimum_coverage']
+ floors={identifier:ceil(floor*sum(row.values())-1e-12) for identifier,row in expected.items()}
+ targets={identifier:ceil(target*sum(row.values())-1e-12) for identifier,row in expected.items()}
+ assigned,_=_global_region_allocation(
+ predicted,expected,floors,targets,contract['minimum_region_passes'])
+ else:
+ assigned={identifier:predicted & row for identifier,row in expected.items()}
+ remaining={identifier:row.copy() for identifier,row in assigned.items()}
+ evidence={identifier:[] for identifier in assigned}
+ lines=[raw.strip() for raw in content.splitlines() if raw.strip()]
+ def assign(line_units,display_lines):
+ shared=line_units.copy()
+ for identifier in sorted(remaining):
+ available=shared if version==3 else line_units
+ contribution=available & remaining[identifier]
+ if not contribution:continue
+ for display in display_lines:
+ if display not in evidence[identifier]:evidence[identifier].append(display)
+ remaining[identifier]-=contribution
+ if version==3:shared-=contribution
+ for line in lines:
+ line_units=Counter(tokens(line) if version==1 else compact_ngrams(line,size))
+ assign(line_units,[line])
+ if version!=1:
+ for left,right in zip(lines,lines[1:]):
+ boundary=(Counter(compact_ngrams(left+right,size))-
+ Counter(compact_ngrams(left,size))-Counter(compact_ngrams(right,size)))
+ assign(boundary,[left,right])
+ if any(remaining.values()):
+ raise ValueError('Body evidence could not locate every allocated scorer unit')
+ return evidence,{identifier:sum(row.values()) for identifier,row in assigned.items()}
+
def _position_label(index,total):
names=['开头','前部','中部','后部','结尾'] if total==5 else [f'位置 {i+1}' for i in range(total)]
start=round(index*100/total);end=round((index+1)*100/total)
@@ -946,10 +987,13 @@ def body_region_rows(case, metrics, output):
contract=case.get('body_requirements') or {}
if not contract:return [],''
recalculated={x['id']:x for x in evaluate_body_requirements(output,contract)['body_region_results']}
+ evidence,evidence_counts=_body_region_evidence(output,contract)
regions={x['id']:x for x in contract['regions']}
rows=[];cards=[];total=len(regions)
for index,item in enumerate(metrics.get('body_region_results',[]),1):
actual={**recalculated.get(item['id'],{}),**item}
+ if evidence_counts.get(item['id'])!=recalculated.get(item['id'],{}).get('matched_units'):
+ raise ValueError('Body evidence allocation differs from scorer: '+item['id'])
expected=regions.get(item['id'],{}).get('text','')
expected_count=actual.get('expected_units',actual.get('expected_tokens',len(expected.split())))
matched=actual.get('matched_units',actual.get('matched_tokens',round(actual['coverage']*expected_count)))
@@ -959,10 +1003,10 @@ def body_region_rows(case, metrics, output):
css='pass' if passed else 'fail'
rows.append(('',cell(esc(label))+cell(f'{expected_count:,}')+cell(f'{matched:,}')+
cell(pct(actual['coverage']))+cell(esc(judgement))))
- passage=_complete_evidence_lines(expected,output)
+ passage='\n\n'.join(evidence.get(item['id'],[]))
actual_html=(f'{esc(passage)}' if passage else
- '完整实际返回中没有找到属于这一段的可读对应文字。
')
- cards.append(f'{index}. {esc(label)}
本段覆盖率为 {pct(actual["coverage"])}。每段至少达到 {pct(actual.get("floor",actual["minimum"]))};达到 {pct(actual["minimum"])} 时算作主要完整段落。
{esc(judgement)}这一位置的完整标准正文{esc(expected)}完整实际返回中的对应文字{actual_html} ')
+ '评分器没有向这一段分配可展示的实际返回文字。
')
+ cards.append(f'{index}. {esc(label)}
本段覆盖率为 {pct(actual["coverage"])}。每段至少达到 {pct(actual.get("floor",actual["minimum"]))};达到 {pct(actual["minimum"])} 时算作主要完整段落。
{esc(judgement)}这一位置的完整标准正文{esc(expected)}参与本段计分的完整实际返回行{actual_html} ')
return rows,cards
def stable_answer_panel(contract):
@@ -1028,7 +1072,7 @@ def stable_anchor_evidence(metrics,output):
alternatives=item.get('alternatives',[]);expected='\n或\n'.join(alternatives)
excerpt=''
for alternative in alternatives:
- candidate=_complete_evidence_lines(alternative,output)
+ candidate=_exact_evidence_lines(alternative,output)
if candidate:excerpt=candidate;break
passed=bool(item.get('pass'));css='pass' if passed else 'fail'
if passed and not excerpt:
@@ -1698,7 +1742,7 @@ def generate(out:Path,dataset:dict,records:list,manifests:list,*,include_english
f'其中至少 {required} 段达到 {pct(rules.get("minimum_coverage"))}。'
'先看下表;需要核对原文时,再展开下面的左右对照。')
body+=section('正文是否抓全',region_intro+table(['标准答案位置','标准片段数','命中片段数','覆盖率','判定'],region_rows,metrics=[])+
- '查看各段完整标准正文和实际对应文字
'+''.join(region_cards)+'','body-coverage')
+ '查看各段完整标准正文和计分行
'+''.join(region_cards)+'','body-coverage')
if title_details:body+=section('标题文字质量',title_details)
runtime=('查看完整运行、哈希与评分字段
'
+metric_legend(detail_metrics)+table(['项目','值'],rows,metrics=[])+
diff --git a/harness/web_fetch/report_i18n.py b/harness/web_fetch/report_i18n.py
index 5a6d00826..9260e4725 100644
--- a/harness/web_fetch/report_i18n.py
+++ b/harness/web_fetch/report_i18n.py
@@ -2979,16 +2979,16 @@ def _register_release_translations() -> None:
"运行日志": "Run logs",
"查看错误与关键日志": "View errors and key log lines",
"没有可展示的逐行日志。": "No line-by-line log is available to display.",
- "查看并下载完整 stdout / stderr / debug 证据": "View and download complete stdout / stderr / debug evidence",
+ "查看并下载完整保存证据": "View and download complete retained evidence",
"下载完整实际输出": "Download complete actual output",
+ "下载完整调用日志 JSON": "Download complete call-log JSON",
"没有可下载的逐行日志文件。": "No line-by-line log file is available to download.",
"查看重试与物理调用": "View retries and physical calls",
- "查看公开适配器字段与证据边界": "View public adapter fields and evidence boundaries",
"原始供应商响应信封仅在内部证据中保留,不进入公开报告或发布包。": "The original provider response envelope is retained only in internal evidence and is excluded from the public report and publication bundle.",
"这一位置的完整标准正文": "Complete reference text for this position",
- "完整实际返回中的对应文字": "Corresponding text in the complete actual output",
- "完整实际返回中没有找到属于这一段的可读对应文字。": "No readable corresponding text for this region was found in the complete actual output.",
- "查看各段完整标准正文和实际对应文字": "View complete reference text and corresponding actual text for each region",
+ "参与本段计分的完整实际返回行": "Complete actual-output lines used to score this region",
+ "评分器没有向这一段分配可展示的实际返回文字。": "The scorer assigned no displayable actual-output text to this region.",
+ "查看各段完整标准正文和计分行": "View complete reference text and scoring lines for each region",
"正文判定:": "Body-text rule: ",
})
@@ -3008,10 +3008,6 @@ def _register_release_translations() -> None:
re.compile(r"^(\d+) 份真实日志文件可以读取;其中至少一份没有在运行记录中声明 SHA-256。$"),
r"\1 real log files are readable; at least one has no SHA-256 declared in the run record.",
))
-REGEX.insert(0, (
- re.compile(r"^下载(.+)完整文件$"),
- r"Download the complete \1 file",
-))
REGEX.insert(0, (
re.compile(r"^正文判定:本例分为 (\d+) 段。每段至少达到 ([\d.]+)%,其中至少 (\d+) 段达到 ([\d.]+)%。先看下表;需要核对原文时,再展开下面的左右对照。$"),
r"Body-text rule: this case has \1 regions. Every region must reach at least \2%, and at least \3 regions must reach \4%. Check the table first, then expand the side-by-side evidence when needed.",
diff --git a/tests/test_report_body_scope.py b/tests/test_report_body_scope.py
index 95675a226..b69bfa7da 100644
--- a/tests/test_report_body_scope.py
+++ b/tests/test_report_body_scope.py
@@ -9,7 +9,7 @@
from unittest.mock import patch
from harness.web_fetch.report import (
- TRACKS, attempt_row, case_rate, generate, overview_card, ranking, reason, stable_anchor_evidence,
+ TRACKS, _body_region_evidence, attempt_row, case_rate, generate, overview_card, ranking, reason, stable_anchor_evidence,
stable_answer_panel, stable_checks, stable_failure_summary, write_support,
)
from harness.web_fetch.report_components import (
@@ -49,6 +49,29 @@ def table_cells(rendered, identifier):
class ReportBodyScopeTests(unittest.TestCase):
+ def test_body_evidence_uses_the_canonical_global_allocation(self):
+ shared='abcdefghijklmnopqrstuvwx'*3
+ contract={'version':3,'unit':'region_compact_hexagram_recall',
+ 'minimum_coverage':.8,'minimum_floor':.6,'minimum_region_passes':1,
+ 'regions':[{'id':'opening','text':shared+'AAAAAAAABBBBBBBB'},
+ {'id':'ending','text':shared+'CCCCCCCCDDDDDDDD'}]}
+ content=shared+'\n'+shared[:30]+'AAAAAAAABBBBBBBBCCCCCCCCDDDDDDDD'
+ evidence,counts=_body_region_evidence(content,contract)
+ from harness.web_fetch.body_coverage import evaluate_body_requirements
+ scored={row['id']:row['matched_units'] for row in
+ evaluate_body_requirements(content,contract)['body_region_results']}
+ self.assertEqual(counts,scored)
+ self.assertTrue(evidence['opening'])
+ self.assertTrue(evidence['ending'])
+
+ def test_body_evidence_keeps_both_lines_for_cross_line_scoring_units(self):
+ contract={'version':3,'unit':'region_compact_hexagram_recall',
+ 'minimum_coverage':.8,'minimum_floor':.6,'minimum_region_passes':1,
+ 'regions':[{'id':'body','text':'abcdefghijkl'}]}
+ evidence,counts=_body_region_evidence('abcdef\nghijkl',contract)
+ self.assertEqual(evidence['body'],['abcdef','ghijkl'])
+ self.assertEqual(counts['body'],7)
+
def test_long_stable_text_and_record_lists_scroll_only_after_five_items(self):
anchors=[{'id':f'a-{index}','alternatives':[f'Stable text {index}']} for index in range(6)]
contract={'minimum_records':6,'maximum_records':6,'stable_text_anchors':anchors}
@@ -305,8 +328,8 @@ def test_body_detail_leads_with_exact_reference_and_output_and_explains_position
self.assertLess(detail.index('标准答案与实际返回'),detail.index('为什么这样判'))
self.assertIn('alpha beta gamma delta',detail)
for phrase in ['正文判定:', '每段至少达到 60.0%',
- '本段覆盖率为 100.0%', '完整实际返回中的对应文字',
- '查看各段完整标准正文和实际对应文字',
+ '本段覆盖率为 100.0%', '参与本段计分的完整实际返回行',
+ '查看各段完整标准正文和计分行',
'查看判定使用的标准答案', '查看本次完整实际返回']:
self.assertIn(phrase,detail)
self.assertNotIn('定位样例(仅供人工查看)',detail)
@@ -317,7 +340,7 @@ def test_body_detail_leads_with_exact_reference_and_output_and_explains_position
self.assertNotIn('第 1/1 段',detail)
english=(out/'en/attempts'/f"{r['attempt_id']}.html").read_text()
for phrase in ['Body-text rule:', 'Every region must reach at least 60.0%',
- 'Corresponding text in the complete actual output']:
+ 'Complete actual-output lines used to score this region']:
self.assertIn(phrase,english)
dataset=(out/'datasets/article.html').read_text()
self.assertIn('同一份标准答案',dataset)
@@ -365,27 +388,40 @@ def test_attempt_call_record_uses_hash_bound_native_log_when_available(self):
self.assertIn('1 份真实日志文件已保存并通过 SHA-256 校验',detail)
self.assertIn(' 1 event=request.start',detail)
self.assertIn('查看错误与关键日志',detail)
- self.assertIn('查看并下载完整 stdout / stderr / debug 证据',detail)
+ self.assertIn('查看并下载完整保存证据',detail)
self.assertIn(r['call_log_sha256'],detail)
- def test_attempt_log_drilldown_accepts_hash_bound_public_debug_stderr(self):
+ def test_attempt_log_drilldown_uses_canonical_call_log_stderr_in_both_languages(self):
c=case('debugged','body','web');r=record(c,passed=False,ok=False)
- debug=b'INFO request started\nERROR challenge page returned\nINFO request ended\n'
- debug_path='logs/fixture/debugged/run-001.stderr.log'
+ payload={'request':{'route':'fixture'},
+ 'process':{'stdout':'adapter started','stderr':'ERROR challenge page returned'},
+ 'error':{'type':'challenge','message':'challenge page returned'},
+ 'events':[{'stage':'call_started','error_type':None},
+ {'stage':'call_completed','error_type':'challenge'}]}
+ debug=json.dumps(payload,ensure_ascii=False).encode()
+ debug_path='call-logs/fixture/debugged/run-001.json'
r['error_type']='challenge';r['error_message']='challenge page returned'
- r['provider_metadata']={'public_stderr_path':debug_path,
- 'public_stderr_sha256':hashlib.sha256(debug).hexdigest()}
+ r['provider_metadata']={'call_log':{'path':debug_path,
+ 'sha256':hashlib.sha256(debug).hexdigest(),
+ 'bytes':len(debug)}}
with tempfile.TemporaryDirectory() as directory:
out=Path(directory);artifact=out/r['artifact_path'];artifact.parent.mkdir(parents=True)
artifact.write_text('output')
log=out/debug_path;log.parent.mkdir(parents=True);log.write_bytes(debug)
with patch('harness.web_fetch.report_explainer.render_explainer',return_value='article'), patch('harness.web_fetch.report.write_support'):
- generate(out,{'cases':[c]},[r],[])
+ generate(out,{'cases':[c]},[r],[],include_english=True)
detail=(out/'attempts'/f"{r['attempt_id']}.html").read_text()
- self.assertIn('stderr / debug 日志',detail)
self.assertIn('ERROR challenge page returned',detail)
self.assertIn('../'+debug_path,detail)
self.assertIn(hashlib.sha256(debug).hexdigest(),detail)
+ self.assertIn('下载完整调用日志 JSON',detail)
+ english=(out/'en/attempts'/f"{r['attempt_id']}.html").read_text()
+ for phrase in ['Final physical call','ERROR challenge page returned',
+ 'Download complete call-log JSON',
+ 'View and download complete retained evidence']:
+ self.assertIn(phrase,english)
+ for phrase in ['最终物理调用','下载完整调用日志','查看并下载完整保存证据']:
+ self.assertNotIn(phrase,english)
def test_minimal_document_scope_is_explicit_and_does_not_claim_long_document_coverage(self):
c=case('minimal','body','document')
From dc65cdd966084c3bfa58d6bfd95c60b782248066 Mon Sep 17 00:00:00 2001
From: milky886
Date: Wed, 23 Sep 2026 20:08:37 +0800
Subject: [PATCH 3/5] Handle multiline body evidence
---
harness/web_fetch/report.py | 53 +++++++++++++++++++--------------
tests/test_report_body_scope.py | 8 +++++
2 files changed, 39 insertions(+), 22 deletions(-)
diff --git a/harness/web_fetch/report.py b/harness/web_fetch/report.py
index cca410086..3bb19c28e 100644
--- a/harness/web_fetch/report.py
+++ b/harness/web_fetch/report.py
@@ -952,29 +952,38 @@ def _body_region_evidence(content,contract):
predicted,expected,floors,targets,contract['minimum_region_passes'])
else:
assigned={identifier:predicted & row for identifier,row in expected.items()}
- remaining={identifier:row.copy() for identifier,row in assigned.items()}
- evidence={identifier:[] for identifier in assigned}
lines=[raw.strip() for raw in content.splitlines() if raw.strip()]
- def assign(line_units,display_lines):
- shared=line_units.copy()
- for identifier in sorted(remaining):
- available=shared if version==3 else line_units
- contribution=available & remaining[identifier]
- if not contribution:continue
- for display in display_lines:
- if display not in evidence[identifier]:evidence[identifier].append(display)
- remaining[identifier]-=contribution
- if version==3:shared-=contribution
- for line in lines:
- line_units=Counter(tokens(line) if version==1 else compact_ngrams(line,size))
- assign(line_units,[line])
- if version!=1:
- for left,right in zip(lines,lines[1:]):
- boundary=(Counter(compact_ngrams(left+right,size))-
- Counter(compact_ngrams(left,size))-Counter(compact_ngrams(right,size)))
- assign(boundary,[left,right])
- if any(remaining.values()):
- raise ValueError('Body evidence could not locate every allocated scorer unit')
+ evidence_lines={identifier:set() for identifier in assigned}
+ if version==1:
+ occurrences=defaultdict(list);flattened=[]
+ for line_index,line in enumerate(lines):
+ for unit in tokens(line):
+ flattened.append(unit);occurrences[unit].append((line_index,))
+ if flattened!=tokens(content):
+ raise ValueError('Body token evidence differs from scorer normalization')
+ for identifier,row in assigned.items():
+ for unit,count in row.items():
+ for location in occurrences[unit][:count]:evidence_lines[identifier].update(location)
+ else:
+ compact_lines=[compact_ngrams(line,1) for line in lines]
+ flattened=[character for line in compact_lines for character in line]
+ if flattened!=compact_ngrams(content,1):
+ raise ValueError('Body fragment evidence differs from scorer normalization')
+ character_lines=[]
+ for line_index,line in enumerate(compact_lines):character_lines.extend([line_index]*len(line))
+ occurrences=defaultdict(list)
+ for index,unit in enumerate(compact_ngrams(content,size)):
+ occurrences[unit].append(tuple(dict.fromkeys(character_lines[index:index+size])))
+ offsets=Counter()
+ for identifier in sorted(assigned):
+ for unit,count in assigned[identifier].items():
+ start=offsets[unit] if version==3 else 0
+ selected=occurrences[unit][start:start+count]
+ if len(selected)!=count:raise ValueError('Body evidence allocation exceeds scorer units')
+ for location in selected:evidence_lines[identifier].update(location)
+ if version==3:offsets[unit]+=count
+ evidence={identifier:[lines[index] for index in sorted(indices)]
+ for identifier,indices in evidence_lines.items()}
return evidence,{identifier:sum(row.values()) for identifier,row in assigned.items()}
def _position_label(index,total):
diff --git a/tests/test_report_body_scope.py b/tests/test_report_body_scope.py
index b69bfa7da..b26d456bb 100644
--- a/tests/test_report_body_scope.py
+++ b/tests/test_report_body_scope.py
@@ -72,6 +72,14 @@ def test_body_evidence_keeps_both_lines_for_cross_line_scoring_units(self):
self.assertEqual(evidence['body'],['abcdef','ghijkl'])
self.assertEqual(counts['body'],7)
+ def test_body_evidence_maps_one_scoring_unit_across_three_short_lines(self):
+ contract={'version':3,'unit':'region_compact_hexagram_recall',
+ 'minimum_coverage':.8,'minimum_floor':.6,'minimum_region_passes':1,
+ 'regions':[{'id':'body','text':'abcdef'}]}
+ evidence,counts=_body_region_evidence('ab\ncd\nef',contract)
+ self.assertEqual(evidence['body'],['ab','cd','ef'])
+ self.assertEqual(counts['body'],1)
+
def test_long_stable_text_and_record_lists_scroll_only_after_five_items(self):
anchors=[{'id':f'a-{index}','alternatives':[f'Stable text {index}']} for index in range(6)]
contract={'minimum_records':6,'maximum_records':6,'stable_text_anchors':anchors}
From 9fb3c27e675586b751911a0f521de34f5d0ce058 Mon Sep 17 00:00:00 2001
From: milky886
Date: Wed, 23 Sep 2026 20:15:06 +0800
Subject: [PATCH 4/5] Remove report renderer redundancy
---
harness/web_fetch/report.py | 5 +++--
harness/web_fetch/report_i18n.py | 1 -
2 files changed, 3 insertions(+), 3 deletions(-)
diff --git a/harness/web_fetch/report.py b/harness/web_fetch/report.py
index 3bb19c28e..e227dd8ee 100644
--- a/harness/web_fetch/report.py
+++ b/harness/web_fetch/report.py
@@ -565,7 +565,8 @@ def run_log_section(out,record,output):
"""Render one in-page, progressively disclosed view of retained run evidence."""
metadata=record.get('provider_metadata') or {}
logs=[]
- for label,binding in call_log_bindings(record):
+ bindings=call_log_bindings(record)
+ for label,binding in bindings:
path=contained_path(out,binding['path'])
if not path.is_file():continue
log_bytes=path.read_bytes();observed=hashlib.sha256(log_bytes).hexdigest()
@@ -576,7 +577,7 @@ def run_log_section(out,record,output):
except (UnicodeDecodeError,json.JSONDecodeError):payload={}
logs.append({'label':label,'path':binding['path'],'text':text,'sha256':observed,
'bytes':len(log_bytes),'bound':bool(binding.get('sha256')),'payload':payload})
- declared_logs=bool(call_log_bindings(record))
+ declared_logs=bool(bindings)
if logs:
availability=(f'{len(logs)} 份真实日志文件已保存并通过 SHA-256 校验。' if all(item['bound'] for item in logs) else
f'{len(logs)} 份真实日志文件可以读取;其中至少一份没有在运行记录中声明 SHA-256。')
diff --git a/harness/web_fetch/report_i18n.py b/harness/web_fetch/report_i18n.py
index 9260e4725..ce9f67d99 100644
--- a/harness/web_fetch/report_i18n.py
+++ b/harness/web_fetch/report_i18n.py
@@ -2985,7 +2985,6 @@ def _register_release_translations() -> None:
"没有可下载的逐行日志文件。": "No line-by-line log file is available to download.",
"查看重试与物理调用": "View retries and physical calls",
"原始供应商响应信封仅在内部证据中保留,不进入公开报告或发布包。": "The original provider response envelope is retained only in internal evidence and is excluded from the public report and publication bundle.",
- "这一位置的完整标准正文": "Complete reference text for this position",
"参与本段计分的完整实际返回行": "Complete actual-output lines used to score this region",
"评分器没有向这一段分配可展示的实际返回文字。": "The scorer assigned no displayable actual-output text to this region.",
"查看各段完整标准正文和计分行": "View complete reference text and scoring lines for each region",
From e0042d1a31c4fa8d04e7ed2c138e372ae0c1ce66 Mon Sep 17 00:00:00 2001
From: milky886
Date: Wed, 23 Sep 2026 20:21:49 +0800
Subject: [PATCH 5/5] Translate final output log label
---
harness/web_fetch/report_i18n.py | 1 +
tests/test_report_body_scope.py | 1 +
2 files changed, 2 insertions(+)
diff --git a/harness/web_fetch/report_i18n.py b/harness/web_fetch/report_i18n.py
index ce9f67d99..40933dad9 100644
--- a/harness/web_fetch/report_i18n.py
+++ b/harness/web_fetch/report_i18n.py
@@ -2977,6 +2977,7 @@ def _register_release_translations() -> None:
"这次历史调用没有保存逐行 debug 日志;不会补造不存在的日志。": "This historical call did not retain a line-by-line debug log; no missing log is reconstructed.",
"运行记录声明了日志文件,但公开证据中没有可读取文件;日志证据不完整。": "The run record declares a log file, but no readable file is present in the public evidence; log evidence is incomplete.",
"运行日志": "Run logs",
+ "最终输出": "Final output",
"查看错误与关键日志": "View errors and key log lines",
"没有可展示的逐行日志。": "No line-by-line log is available to display.",
"查看并下载完整保存证据": "View and download complete retained evidence",
diff --git a/tests/test_report_body_scope.py b/tests/test_report_body_scope.py
index b26d456bb..f27464296 100644
--- a/tests/test_report_body_scope.py
+++ b/tests/test_report_body_scope.py
@@ -425,6 +425,7 @@ def test_attempt_log_drilldown_uses_canonical_call_log_stderr_in_both_languages(
self.assertIn('下载完整调用日志 JSON',detail)
english=(out/'en/attempts'/f"{r['attempt_id']}.html").read_text()
for phrase in ['Final physical call','ERROR challenge page returned',
+ 'Final output',
'Download complete call-log JSON',
'View and download complete retained evidence']:
self.assertIn(phrase,english)