{esc(item["label"])}
{item["bytes"]:,} 字节 · SHA-256:{esc(item["sha256"])}
{esc(_call_log_excerpt(item))}'
+ f'diff --git a/harness/web_fetch/report.py b/harness/web_fetch/report.py index 4434ea4a5..e227dd8ee 100644 --- a/harness/web_fetch/report.py +++ b/harness/web_fetch/report.py @@ -561,12 +561,12 @@ def empty_output_reason(record,case,output): return '最终输出确实为空。上游请求有响应,但适配器没有得到可提取的正文。' return '最终输出确实为空;本次历史记录没有保存更具体的原因,不能据此推断是服务端、网络还是适配器问题。' -def call_log_dialog(out,record,case,output): - """Render one honest drill-down view from a native log or legacy result fields.""" +def run_log_section(out,record,output): + """Render one in-page, progressively disclosed view of retained run evidence.""" metadata=record.get('provider_metadata') or {} - public_metadata=public_provider_metadata(metadata) 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,53 +576,59 @@ def call_log_dialog(out,record,case,output): try:payload=json.loads(log_bytes) except (UnicodeDecodeError,json.JSONDecodeError):payload={} logs.append({'label':label,'path':binding['path'],'text':text,'sha256':observed, - 'bound':bool(binding.get('sha256')),'payload':payload}) + 'bytes':len(log_bytes),'bound':bool(binding.get('sha256')),'payload':payload}) + declared_logs=bool(bindings) if logs: - native=(f'{len(logs)} 个物理调用日志已保存并通过 SHA-256 校验。' if all(item['bound'] for item in logs) else - '事件日志文件可以读取,但至少一条记录未声明其 SHA-256;页面会展示内容,但不能证明全部日志均与运行记录绑定。') - elif call_log_bindings(record): - native='记录声明了事件日志,但公开证据中没有可读取文件;下面仅展示可核验的结果字段。' + availability=(f'{len(logs)} 份真实日志文件已保存并通过 SHA-256 校验。' if all(item['bound'] for item in logs) else + f'{len(logs)} 份真实日志文件可以读取;其中至少一份没有在运行记录中声明 SHA-256。') + elif declared_logs: + availability='运行记录声明了日志文件,但公开证据中没有可读取文件;日志证据不完整。' else: - native='这次历史运行没有保存统一事件日志。下面由最终输出、结果字段和公开适配器元数据合成,不能还原未记录的逐行过程。' + availability='这次历史调用没有保存逐行 debug 日志;不会补造不存在的日志。' final_log=logs[-1]['payload'] if logs else {} log_error=final_log.get('error') if isinstance(final_log.get('error'),dict) else {} log_process=final_log.get('process') if isinstance(final_log.get('process'),dict) else {} error_type=record.get('error_type') or log_error.get('type') or '—(未记录)' error_message=(record.get('error_message') or log_error.get('message') or log_process.get('stderr') or ('无报错;请求记录为成功。' if record.get('ok') else '请求失败,但历史记录未保存错误文字。')) - error_class=' call-log-error' if record.get('error_type') or record.get('error_message') or not record.get('ok') else '' - if output: - output_html=f'
{esc(readable_output(output))}'
- else:
- output_html=f'SHA-256:{esc(item["sha256"])}
{esc(item["text"])}本次没有登记重试;只保留一个物理调用。
' if not retry else - f'{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_data=artifact.read_bytes() if artifact.is_file() else output.encode('utf-8')
+ output_bytes=len(output_data);output_sha=hashlib.sha256(output_data).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'{item["bytes"]:,} 字节 · SHA-256:{esc(item["sha256"])}
{esc(_call_log_excerpt(item))}'
+ f'没有可展示的逐行日志。
' + download_html=''.join( + f'下载完整调用日志 JSON · '
+ f'{item["bytes"]:,} 字节 · SHA-256:{esc(item["sha256"])}
没有可下载的逐行日志文件。
' + retry_html=('本次没有登记重试;只保留一个物理调用。
' if not retry else + f'{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'日志可用性:{esc(availability)}
' + f'最后报错:{esc(error_type)} · {esc(error_message)}
' + f'{output_link}
{download_html}{esc(excerpt)}' if excerpt else
- '没有可供人工定位的连续文字样例;机器覆盖率仍按上方完整实际返回计算。
') - cards.append(f'这一段标准答案产生 {expected_count:,} 个连续六字符片段;在上方完整实际返回({len(output):,} 字符)中命中 {matched:,} 个,所以覆盖率为 {matched:,} ÷ {expected_count:,} = {pct(actual["coverage"])}。目标 {pct(actual["minimum"])},硬下限 {pct(actual.get("floor",actual["minimum"]))}。
{esc(expected)}这是从完整实际返回中选出的一小段,便于确认文字确实出现;它不是覆盖率计算所使用的全部实际输出。
{actual_html}{esc(passage)}' if passage else
+ '评分器没有向这一段分配可展示的实际返回文字。
') + cards.append(f'本段覆盖率为 {pct(actual["coverage"])}。每段至少达到 {pct(actual.get("floor",actual["minimum"]))};达到 {pct(actual["minimum"])} 时算作主要完整段落。
{esc(expected)}{esc(count)}
共 {len(anchors)} 组稳定文字,全部属于标准答案;当前区域显示约 5 组高度,向下滚动查看全部。
' + if len(anchors)>5 else '') + return note+f'{esc(count)}
{esc(excerpt)}
' if excerpt else '实际返回中未找到可供定位的连续原文。
') cards.append(f'{esc(expected)}
共 {len(cards)} 项稳定文字判定,全部参与评分;当前区域显示约 5 项高度,向下滚动查看全部。
' + if len(cards)>5 else '') + scroll=' data-scroll-after="5"' if len(cards)>5 else '' + return note+f'本例未设置稳定文字核对点,不作文字内容已核验的结论。
' records=[('',cell(esc(json.dumps(x.get('fields',{}),ensure_ascii=False)))+cell('通过' if x.get('valid') else '未通过')+cell(esc(';'.join(x.get('errors',[])) or '—'))) for x in m.get('stable_records',[])] - return content+table(['条目字段','结构判定','原因'],records)+('未通过原因:'+esc(stable_failure_summary(m))+'。
' if m.get('stable_task_errors') else '') + record_table=table(['条目字段','结构判定','原因'],records) + if len(records)>5:record_table=f'未通过原因:'+esc(stable_failure_summary(m))+'。
' if m.get('stable_task_errors') else '') def ranking(records,cases,track,base='',id=None,compact=False,audit=None): from .report_components import comparable_body_reference @@ -1528,7 +1657,7 @@ def generate(out:Path,dataset:dict,records:list,manifests:list,*,include_english summary+=('查看判定使用的标准答案' '查看本次完整实际返回' '查看逐项判定' - '
') + '查看运行日志与证据') if r.get('diagnosis') and not passed: d=r['diagnosis'];s=d.get('independent_source_check') or {} quote_issue=_quote_field_issue(m) @@ -1618,20 +1747,19 @@ def generate(out:Path,dataset:dict,records:list,manifests:list,*,include_english region_rows,region_cards=body_region_rows(c,m,output);rules=c.get('body_requirements',{}) required=rules.get('minimum_region_passes',len(rules.get('regions',[]))) region_count=len(rules.get('regions',[])) - region_intro=(f'{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..40933dad9 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,63 @@ 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", + "最终输出": "Final output", + "查看错误与关键日志": "View errors and key log lines", + "没有可展示的逐行日志。": "No line-by-line log is available to display.", + "查看并下载完整保存证据": "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", + "原始供应商响应信封仅在内部证据中保留,不进入公开报告或发布包。": "The original provider response envelope is retained only in internal evidence and is excluded from the public report and publication bundle.", + "参与本段计分的完整实际返回行": "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: ", }) +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"^正文判定:本例分为 (\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..f27464296 100644
--- a/tests/test_report_body_scope.py
+++ b/tests/test_report_body_scope.py
@@ -9,8 +9,8 @@
from unittest.mock import patch
from harness.web_fetch.report import (
- TRACKS, attempt_row, case_rate, generate, overview_card, ranking, reason, stable_anchor_evidence,
- stable_failure_summary, write_support,
+ 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 (
task_contract_guide, task_pass_guide, examples, list_rule_description, metric_guide, reference_status, task_instruction,
@@ -49,6 +49,52 @@ 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_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}
+ 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 +331,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%',
+ 'Complete actual-output lines used to score this region']:
+ self.assertIn(phrase,english)
dataset=(out/'datasets/article.html').read_text()
self.assertIn('同一份标准答案',dataset)
self.assertNotIn('字符六元组数',dataset)
@@ -315,16 +368,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"','