Required body.
'+model_text('按本次所列任务的通过率排序;未完成仓库认证流程。测试日期和核验范围见详情。','Ordered by pass rate on the listed tasks; repository certification is incomplete. See details for test dates and verification scope.')+'
'
note='已有复核记录;覆盖和裁定依据见核验页面。' if dataset['comparison'].get('independent_review') else '结果为机器判定,未进行独立人工审核。'
return ''+hero('Web Fetch 评测报告',description)
from .report_explainer import article_title,render_explainer,supports_authored_explainer
TITLE='本轮调用链检查与日志范围' if dataset.get('comparison',{}).get('call_chain_audit') else article_title(records) if authored_interpretation and supports_authored_explainer(cases) and not all(row.get('provider_type') == 'fixture' for row in records) else '本报告的任务范围与验收规则'
@@ -1940,7 +1944,8 @@ def generate(out:Path,dataset:dict,records:list,manifests:list,*,include_english
coverage=f'{len(selected)} 个案例 · {len({r["provider"] for r in records if cases[r["case_id"]].get("track","web")==track})} 种已测方法'
panels.append((track,label,f'
{label}
{coverage}'+ranking(records,cases,track,compact=True,audit=audit,diagnostic=diagnostic)+f'
{link("datasets/index.html#track-"+track,"案例与参考 →")}
'))
home+=section(f'{track_label} 类任务概览',('' if local_pass_rate_ranking(dataset) else review_summary(out))+'
调用完整:'+str(reliability['expected_calls'])+'/'+str(reliability['expected_calls'])+' 次均有结果。
只在同一任务类别内比较。
'+cards+'
','overview')
- home+=section('任务判定汇总(非排名)' if diagnostic else '综合通过率总榜',overall_ranking(records,cases,audit,diagnostic=diagnostic),'overall-ranking')
+ overall_title=('任务判定汇总(非排名)' if diagnostic else model_text('本轮任务通过率排名','Task pass-rate ranking for this sample') if local_pass_rate_ranking(dataset) else '综合通过率总榜')
+ home+=section(overall_title,overall_ranking(records,cases,audit,diagnostic=diagnostic),'overall-ranking')
if diagnostic:
home+=result_ledger_section(out,records)
low=[r for r in records if not r['metrics'].get('evaluation_excluded') and succeeded(r,cases[r['case_id']]) and cases[r['case_id']].get('ground_truth_main_content') and r['metrics'].get('word_precision') is not None and r['metrics']['word_precision']<.3]
@@ -2288,8 +2293,8 @@ def current_local_review_findings(out,dataset,records,audit):
f"Currently {passed} pass among {len(eligible)} eligible results; {len(records)-len(eligible)} are excluded. Pages and downloadable summaries use the same saved results."),
(f"保留独立复核记录 {audit.get('reviewed_attempts',0)}/{len(records)};另有 {unreviewed} 条未独立复核,其中机器通过 {audit.get('accepted_unreviewed_passes',0)} 条。补充裁定 {adjudicated} 条,不称为新一轮全量盲审。",
f"Retained independent review covers {audit.get('reviewed_attempts',0)}/{len(records)} results. Another {unreviewed} are not independently reviewed, including {audit.get('accepted_unreviewed_passes',0)} machine passes. There are {adjudicated} supplemental adjudications; this is not a new full blind review."),
- (f"评分规则版本为 {dataset['comparison'].get('scoring_policy_version',1)}。目标内容要求、正文质量和验证提示分别展示;提示文字不单独决定任务是否通过。",
- f"Scoring policy version: {dataset['comparison'].get('scoring_policy_version',1)}. Target requirements, body quality and verification hints are reported separately; hint words alone do not determine acceptance."),
+ ("目标内容要求、正文质量和诊断文字分别展示;具体评分实现哈希见各结果详情,诊断词不单独决定任务是否通过。",
+ "Target requirements, body quality and diagnostic text are reported separately. Each result binds its scorer hash; diagnostic words alone do not determine acceptance."),
]
if local_pass_rate_ranking(dataset):
rows.append(('名次按当前任务通过率计算,耗时单独展示,不参与名次。历史调用未完成调用前后的全量来源核验,保留实际检查日期和范围,不声称已完成来源认证。',
diff --git a/harness/web_fetch/report_build.py b/harness/web_fetch/report_build.py
index 011ccb520..05e1a87aa 100644
--- a/harness/web_fetch/report_build.py
+++ b/harness/web_fetch/report_build.py
@@ -229,6 +229,10 @@ def _build_frozen_presentation(source, output, presentation_binding, *,
shutil.copy2(presentation_binding, staging / "presentation-binding.json")
batches = json.loads((source / "execution.json").read_text())["batches"]
for record in records:
+ case = next(item for item in dataset['cases'] if item['id'] == record['case_id'])
+ if not record.get('diagnosis'):
+ text = contained_path(source, record['artifact_path']).read_text(errors='replace')
+ record['diagnosis'] = diagnose(record, case, text)
metadata = record.setdefault("provider_metadata", {})
metadata["public_log_note"] = dataset.get("comparison", {}).get(
"call_chain_audit", {}).get("call_log_notes", {}).get(record["attempt_id"], "")
diff --git a/harness/web_fetch/report_check.py b/harness/web_fetch/report_check.py
index eaff454eb..be4d71a68 100644
--- a/harness/web_fetch/report_check.py
+++ b/harness/web_fetch/report_check.py
@@ -55,12 +55,14 @@ def require_table_index_headers(parser, *, english=False, diagnostic=False):
raise ValueError('Unexpected table index header: ' + header)
-def require_home_comparison_layout(home, track_count, *, diagnostic=False):
- overall = '任务判定汇总(非排名)' if diagnostic else '综合通过率总榜'
+def require_home_comparison_layout(home, track_count, *, diagnostic=False, sample_ranking=False):
+ overall = ('任务判定汇总(非排名)' if diagnostic else
+ '本轮任务通过率排名' if sample_ranking else '综合通过率总榜')
details = '按任务类别查看诊断结果' if diagnostic else f'{track_count} 类分榜'
- markers = [f'
{track_count} 类任务概览
', f'
{overall}
', f'
{details}
']
- if any(marker not in home for marker in markers) or not (
- home.index(markers[0]) < home.index(markers[1]) < home.index(markers[2])):
+ markers = [f'{track_count} 类任务概览', overall, details]
+ headings = [re.sub(r'<[^>]+>', '', value) for value in re.findall(r'
]*>(.*?)
', home, re.S)]
+ if any(marker not in headings for marker in markers) or not (
+ headings.index(markers[0]) < headings.index(markers[1]) < headings.index(markers[2])):
raise ValueError('Task overview and comparison must precede detailed results')
if '判分完整性' not in home or ('本类最高:' in home) == diagnostic:
raise ValueError('Comparison winner disclosure differs from evidence state')
@@ -188,15 +190,48 @@ def require_replacement_execution(root, binding, physical, logical):
raise ValueError('Task replacement physical/logical call inventory differs')
+class VisibleLocalScope(HTMLParser):
+ """Disclosure attributes count only when attached to visible explanatory text."""
+ def __init__(self):
+ super().__init__()
+ self.stack = []
+ self.visible = set()
+
+ def handle_starttag(self, tag, attrs):
+ attrs = dict(attrs)
+ inherited = self.stack[-1][1:] if self.stack else (False, set())
+ style = re.sub(r'\s+', '', attrs.get('style', '')).lower()
+ hidden = (inherited[0] or tag in {'script', 'style', 'template'}
+ or 'hidden' in attrs or attrs.get('aria-hidden') == 'true'
+ or 'display:none' in style or 'visibility:hidden' in style)
+ kinds = set(inherited[1])
+ if attrs.get('data-evidence-state') == 'local_measurement': kinds.add('evidence')
+ if attrs.get('data-ranking-scope') == 'saved-task-pass-rate': kinds.add('ranking')
+ if tag not in {'area','base','br','col','embed','hr','img','input','link','meta','param','source','track','wbr'}:
+ self.stack.append((tag, hidden, kinds))
+
+ def handle_endtag(self, tag):
+ for index in range(len(self.stack) - 1, -1, -1):
+ if self.stack[index][0] == tag:
+ del self.stack[index:]
+ break
+
+ def handle_data(self, data):
+ if data.strip() and self.stack and not self.stack[-1][1]:
+ self.visible.update(self.stack[-1][2])
+
+
def check_local_diagnostic_presentation(root: Path, *, ranked=False):
"""Track evidence state and scope any explicitly requested sample ranking."""
for path in root.rglob('*.html'):
page = path.read_text(encoding='utf-8')
if '
' not in page:
continue # Original response/source HTML is evidence, not report prose.
- if 'data-evidence-state="local_measurement"' not in page:
+ disclosure = VisibleLocalScope()
+ disclosure.feed(page)
+ if 'evidence' not in disclosure.visible:
raise ValueError('Local diagnostic disclosure missing: ' + str(path.relative_to(root)))
- if ranked and 'data-ranking-scope="saved-task-pass-rate"' not in page:
+ if ranked and 'ranking' not in disclosure.visible:
raise ValueError('Sample ranking scope missing: ' + str(path.relative_to(root)))
for table in re.findall(r'
]*\bid="ranking-[^"]+".*?
', page, re.S):
if not ranked and ('data-dir="desc"' in table or re.search(r'
\s*(?:序号|Rank|No\.)\s* | ', table)):
@@ -590,7 +625,7 @@ def canonical_link(page, value):
raise ValueError("Report acceptance check failed")
home = (root / "index.html").read_text()
track_count = len({case.get("track", "web") for case in d["cases"]})
- require_home_comparison_layout(home, track_count, diagnostic=local_measurement and not sample_ranking)
+ require_home_comparison_layout(home, track_count, diagnostic=local_measurement and not sample_ranking, sample_ranking=sample_ranking)
if (root / "resource-cohort/resource-comparison.json").is_file():
guide = (root / "guide/index.html").read_text()
for page_name, text in [("home", home), ("guide", guide)]:
diff --git a/harness/web_fetch/report_i18n.py b/harness/web_fetch/report_i18n.py
index 6a628c100..5141553c0 100644
--- a/harness/web_fetch/report_i18n.py
+++ b/harness/web_fetch/report_i18n.py
@@ -2458,6 +2458,7 @@
# are deliberately narrow report phrases; they never include task names,
# reference answers, or provider output.
PHRASES = {
+ "处理路线:": "Retrieval path: ",
"的 Moli 已保存输出仍包含": "'s saved Moli output still contains ",
",内容规则未通过。请求成功在这里记录了一个正常返回的结果,却没有等来任务要求的内容。这比方法是否具备浏览器功能更接近本次接入要解决的问题。": ", but failed the content rule. A normal response did not wait for the required content. This is closer to the integration problem than whether the method has browser features.",
"Quotes to Scrape 的": "Quotes to Scrape's ",
@@ -2594,6 +2595,17 @@
TEXT.update(adoption_text)
+def _body_region_summary(match):
+ regions = match[1]
+ for source, target in (("开头", "Opening"), ("前部", "Early section"),
+ ("中部", "Middle"), ("后部", "Late section"),
+ ("结尾", "Ending"), ("位置", "Region")):
+ regions = regions.replace(source + " ", target + " ")
+ return ("Body coverage is incomplete. Regions below target: "
+ + regions.replace("、", ", ")
+ + ". The complete reference and actual output are shown side by side below.")
+
+
REGEX = [
(re.compile(r"^本报告包含 (\d+) 个任务、(\d+) 个逻辑结果,任务通过 (\d+)/(\d+)。结论仅适用于已登记的任务和规则。$"), r"This report contains \1 tasks and \2 logical results, with \3/\4 task passes. Conclusions apply only to the registered tasks and rules."),
(re.compile(r"^的 F1 为 ([\d.]+%),但没有满足该例任务规则。零通过表示没有一次满足全部任务门槛,不表示输出与参考毫无词语重合。这里的 F1 对照已保存的样本文本,只诊断文字重合,不承担实时事实核验。词频相似度允许部分文字得分,任务规则检查指定的内容或字段结构;二者应保留各自的含义。$"),
@@ -2760,7 +2772,7 @@
(re.compile(r"^实际解析出 (\d+) 条完整且不重复的记录,要求至少 (\d+) 条;结构检查未通过,稳定文字检查未通过。$"), r"Parsed \1 complete, unique records against a minimum of \2; structure and stable-text checks failed."),
(re.compile(r"^实际解析出 (\d+) 条完整且不重复的记录,要求至少 (\d+) 条;结构检查(通过|未通过),稳定文字检查(通过|未通过)。$"), lambda m: f"Parsed {m.group(1)} complete, unique records against a minimum of {m.group(2)}; structure {'passed' if m.group(3) == '通过' else 'failed'} and stable-text checks {'passed' if m.group(4) == '通过' else 'failed'}."),
(re.compile(r"^完整度 / 纯净度 / F1 为 ([^。]+)。$"), r"Recall / precision / F1: \1."),
- (re.compile(r"^正文没有抓全;低于目标的位置为:(.+)。下方会把完整标准答案与实际返回并排展示。$"), lambda m: "Body coverage is incomplete. Regions below target: " + m[1].replace("位置 ", "region ") + ". The complete reference and actual output are shown side by side below."),
+ (re.compile(r"^正文没有抓全;低于目标的位置为:(.+)。下方会把完整标准答案与实际返回并排展示。$"), _body_region_summary),
(re.compile(r"^这一段标准答案产生 ([\d,]+) 个连续六字符片段;在上方完整实际返回(([\d,]+) 字符)中命中 ([\d,]+) 个,所以覆盖率为 ([\d,]+) ÷ ([\d,]+) = ([\d.]+%)。目标 ([\d.]+%),硬下限 ([\d.]+%)。$"), r"This reference region produces \1 contiguous six-character shingles. \3 are found in the complete output above (\2 characters), so coverage is \4 ÷ \5 = \6. Target: \7; hard floor: \8."),
(re.compile(r"^· 采集时间 (.+)$"), r"· captured at \1"),
]
diff --git a/harness/web_fetch/report_quality.py b/harness/web_fetch/report_quality.py
index 6d2a9ee10..e9033564d 100644
--- a/harness/web_fetch/report_quality.py
+++ b/harness/web_fetch/report_quality.py
@@ -14,7 +14,7 @@ def quality_observations(record, case, output=''):
and re.search(r'hcaptcha\.com/(?:privacy|what-is-hcaptcha)', output, re.I))
return {'task_pass': task_passed(record['ok'], metrics, case.get('task_kind')),
'word_precision': precision, 'low_precision': precision is not None and precision < .3,
- 'hint_words': list(dict.fromkeys(metrics.get('forbidden_hits', []) + metrics.get('blocked_page_hits', []))),
+ 'hint_words': list(dict.fromkeys(metrics.get('diagnostic_hits', []) + metrics.get('blocked_page_hits', []))),
'challenge_component_text': component}
diff --git a/harness/web_fetch/report_reading.py b/harness/web_fetch/report_reading.py
index 9719c28c1..a4787fdad 100644
--- a/harness/web_fetch/report_reading.py
+++ b/harness/web_fetch/report_reading.py
@@ -61,6 +61,8 @@ def sanitize_article_html(fragment):
if element.name == 'a' and 'href' in attrs:
attrs['rel'] = 'noopener noreferrer'
element.attrs = attrs
+ for table in soup.find_all('table'):
+ table.wrap(soup.new_tag('div', attrs={'class': 'table-wrap'}))
return str(soup)
diff --git a/harness/web_fetch/report_style.py b/harness/web_fetch/report_style.py
index 0f505332c..3b05df79e 100644
--- a/harness/web_fetch/report_style.py
+++ b/harness/web_fetch/report_style.py
@@ -1,5 +1,6 @@
"""Shared visual styling for the current report and standalone editorial export."""
STYLE = r"""
+[data-archived-reading] img{max-width:100%;height:auto}
:root{color-scheme:light;--bg:#f6f8fb;--panel:#fff;--panel2:#eef3f8;--ink:#17212b;--muted:#5d6b78;--line:#d9e2ea;--accent:#1268d8;--accent-soft:#e9f2ff;--violet:#6756d8;--lime:#3f7b26;--coral:#bd402f;--good:#117a47;--bad:#c22e43;--warn:#8a5a00;--callout:#fff6d8;--failure-bg:#fff0f2;--success-bg:#eaf8f0;--code-bg:#101820;--code-ink:#e9f1f5;--shadow:0 8px 28px rgba(30,55,80,.08);--max:1180px}
:root[data-theme="dark"]{color-scheme:dark;--bg:#071015;--panel:#0d1a21;--panel2:#122630;--ink:#edf6f7;--muted:#9aadb4;--line:#263c46;--accent:#49d6ed;--accent-soft:#10303a;--violet:#8b7cff;--lime:#c6ed3d;--coral:#ff8168;--good:#67df9f;--bad:#ff6b7a;--warn:#ffd166;--callout:#251f13;--failure-bg:#2a1419;--success-bg:#10271d;--code-bg:#050d11;--code-ink:#d7e8eb;--shadow:none}
*{box-sizing:border-box}html{scroll-behavior:smooth}body{margin:0;background:linear-gradient(180deg,var(--accent-soft) 0,transparent 240px),var(--bg);color:var(--ink);font:15px/1.65 Inter,system-ui,-apple-system,"Segoe UI","PingFang SC",sans-serif;transition:background .2s,color .2s}a{color:var(--accent);text-decoration:none}a:hover{text-decoration:underline}.wrap{width:min(var(--max),calc(100% - 40px));margin:auto}.top{border-bottom:1px solid var(--line);background:color-mix(in srgb,var(--panel) 92%,transparent);position:sticky;top:0;z-index:5;backdrop-filter:blur(14px)}nav{display:flex;align-items:center;gap:17px;overflow:auto;padding:10px 0;white-space:nowrap}.brand{font-weight:850;color:var(--ink);margin-right:auto}.theme-toggle{border:1px solid var(--line);background:var(--panel);color:var(--ink);border-radius:999px;padding:7px 12px;font:inherit;font-size:12px;cursor:pointer}.theme-toggle:hover{border-color:var(--accent)}.hero{padding:42px 0 30px}.eyebrow{font:750 12px/1.2 ui-monospace,monospace;letter-spacing:.12em;color:var(--accent)}h1{font-size:clamp(34px,5vw,58px);line-height:1.08;letter-spacing:-.04em;margin:13px 0;max-width:980px}h2{font-size:clamp(25px,3vw,36px);line-height:1.16;letter-spacing:-.025em;margin:0 0 12px}h3{font-size:19px;margin:24px 0 8px}.lead,.sub{color:var(--muted);max-width:900px}.meta{display:flex;gap:8px;flex-wrap:wrap}.pill,.tag{border:1px solid var(--line);background:var(--panel);border-radius:999px;padding:5px 10px;color:var(--muted);font-size:12px}.tag{padding:2px 7px}.grid{display:grid;grid-template-columns:repeat(12,1fr);gap:14px}.card{background:var(--panel);border:1px solid var(--line);border-radius:14px;padding:20px;box-shadow:var(--shadow)}.kpi{grid-column:span 3;border-top:3px solid var(--accent)}.kpi:nth-child(2){border-top-color:var(--violet)}.kpi:nth-child(3){border-top-color:var(--lime)}.kpi:nth-child(4){border-top-color:var(--coral)}.n{font-size:32px;font-weight:850;letter-spacing:-.04em;margin:7px 0}.section{padding:38px 0;border-top:1px solid var(--line)}.table-wrap{overflow:auto;border:1px solid var(--line);border-radius:13px;background:var(--panel);box-shadow:var(--shadow)}table{border-collapse:collapse;width:100%;min-width:760px;background:var(--panel)}th,td{padding:11px 13px;border-bottom:1px solid var(--line);text-align:left;vertical-align:top}th{background:var(--panel2);font-size:12px;letter-spacing:.03em;position:sticky;top:0}td{color:var(--ink)}tbody tr:hover{background:var(--accent-soft)}.good{color:var(--good);font-weight:750}.bad{color:var(--bad);font-weight:750}.warn{color:var(--warn);font-weight:750}.bar{height:9px;background:var(--panel2);border-radius:99px;overflow:hidden;min-width:120px}.bar>i{display:block;height:100%;background:linear-gradient(90deg,var(--accent),var(--violet));border-radius:99px}.callout{border-left:4px solid var(--warn);background:var(--callout);padding:14px 17px;border-radius:0 12px 12px 0}.failure{border-left-color:var(--bad);background:var(--failure-bg)}.success{border-left-color:var(--good);background:var(--success-bg)}pre,code{font-family:ui-monospace,SFMono-Regular,Menlo,monospace}code{color:var(--accent);overflow-wrap:anywhere}pre{white-space:pre-wrap;overflow-wrap:anywhere;background:var(--code-bg);border:1px solid var(--line);padding:18px;border-radius:12px;color:var(--code-ink);max-height:70vh;overflow:auto}.attempt-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(190px,1fr));gap:10px}.kv{background:var(--panel);border:1px solid var(--line);border-radius:11px;padding:13px}.kv b{display:block;font-size:12px;color:var(--muted);margin-bottom:4px}.footer{padding:36px 0 64px;color:var(--muted)}.empty{color:var(--muted);font-style:italic}.tiny{font-size:12px;color:var(--muted)}
diff --git a/harness/web_fetch/scoring.py b/harness/web_fetch/scoring.py
index 9cfb9192d..1069fe263 100644
--- a/harness/web_fetch/scoring.py
+++ b/harness/web_fetch/scoring.py
@@ -13,8 +13,6 @@
from .models import EvalCase
from .safety import contained_path
-SCORING_POLICY_VERSION = 2
-
# Bind orchestration as well as the underlying deterministic matchers. Computed
# once at import, shared by live/rescore output; never a reference-freshness claim.
@@ -100,9 +98,8 @@ def score_attempt(
evidence is preserved; the caller must have validated its artifact binding.
"""
case = _case_dict(case)
- policy_version = case.get('acceptance_policy_version', SCORING_POLICY_VERSION)
- if type(policy_version) is not int or policy_version not in {1, 2}:
- raise ValueError('Unsupported scoring policy version')
+ if 'acceptance_policy_version' in case:
+ raise ValueError('Historical policy selectors require their frozen scorer; current scoring has one contract')
contract = case.get('acceptance_contract') or {}
if contract.get('content_scope') == 'full_page_stable_coverage':
if (contract.get('minimum_records', 0) == 0
@@ -116,8 +113,9 @@ def score_attempt(
result = {key: value for key, value in (previous_metrics or {}).items()
if key not in _CONDITIONAL_FIELDS
and not key.startswith(("stable_", "list_", "page_context_"))}
- result['acceptance_policy_version'] = policy_version
- keys = ("required_facts", "forbidden_any", "min_content_chars",
+ for retired in ('acceptance_policy_version', 'forbidden_hits', 'reference_term_exemptions'):
+ result.pop(retired, None)
+ keys = ("required_facts", "forbidden_any", "diagnostic_any", "min_content_chars",
"fact_coverage_threshold", "ground_truth_main_content", "required_patterns",
"acceptance_contract", "body_requirements", "composite_acceptance")
task_kind = case.get("task_kind", "body")
@@ -315,12 +313,7 @@ def score_attempt(
# interpretation of continuous similarity as a pass rate.
if 'ordered_tree_min_similarity' in case:
result['content_pass'] = bool(
- ok and tree_result['score'] >= case['ordered_tree_min_similarity'])
+ ok and tree_result['score'] >= case['ordered_tree_min_similarity']
+ and not result['forbidden_requirement_hits'])
result["scoring_source_sha256"] = SCORING_SOURCE_SHA256
- if policy_version == 1 and (result.get('forbidden_hits') or result.get('blocked_page')
- or result.get('stable_blocked_page')):
- result['content_pass'] = False
- if 'stable_task_pass' in result:
- result['stable_task_pass'] = False
- result['stable_content_pass'] = False
return result
diff --git a/harness/web_fetch/scoring_regressions.py b/harness/web_fetch/scoring_regressions.py
index 84cbb520a..333daec15 100644
--- a/harness/web_fetch/scoring_regressions.py
+++ b/harness/web_fetch/scoring_regressions.py
@@ -32,14 +32,9 @@ def load_scoring_regressions(path: Path | None = None) -> dict[str, dict[str, An
rows: dict[str, dict[str, Any]] = {}
if document.get('control_revisions'):
raise ValueError('Historical controls cannot override inputs or expected results; add new IDs')
- historical = document.get('historical_policy_versions', {})
- if (not isinstance(historical, dict)
- or set(historical) - {row.get('id') for row in document['cases']}
- or any(type(value) is not int or value != 1 for value in historical.values())):
- raise ValueError('Invalid historical scoring policy versions')
+ if document.get('historical_policy_versions'):
+ raise ValueError('Historical policy selectors belong in frozen scorer snapshots')
for index, row in enumerate(document["cases"], 1):
- if row.get('id') in historical:
- row = {**row, 'case': {**row['case'], 'acceptance_policy_version': historical[row['id']]}}
identifier = row.get("id")
if not isinstance(identifier, str) or not identifier.strip() or identifier in rows:
raise ValueError(f"Scoring regression {index} requires a unique nonempty id")
diff --git a/research/native-430-local-report.json b/research/native-430-local-report.json
index b65eecc1e..e4f2a8f81 100644
--- a/research/native-430-local-report.json
+++ b/research/native-430-local-report.json
@@ -15,8 +15,8 @@
"openclaw-built-in",
"tavily"
],
- "frozen_dataset_sha256": "12952a88e6e0d735830d802436d4030bf9c5d4c31c0f3cf7fa4a8ef77206fd0b",
- "results_sha256": "da69b585066531c4412a0a646e11fd899557964f9b04529a0545cdea7e36e2a4",
+ "frozen_dataset_sha256": "ff578deac41e00056655542c139611d21fd26bbd36a86e9a8628c35d17356490",
+ "results_sha256": "b0bcc2da22ca9fc7e495affb2c25a99c8d56fe89024d12f8c615e97c43b37a1b",
"measured_scoring_source_sha256": "4ce40cdf2ecb1af4088915a2b869ffbdcb65dce5222d1429578e97d175f13bfc",
"evidence_state": "local_measurement",
"certified": false,
@@ -28,7 +28,7 @@
},
{
"path": "result-review.json",
- "sha256": "9f1c5cfd4fee7bf3a6a51490a4c1bfc5a685dcacabbbe35a0fe50581836199e1"
+ "sha256": "0f9190fa85516469740fab16b2786adc092bc26b2a31f13af74a48b1ee4af5bf"
},
{
"path": "archived-reading.json",
@@ -39,14 +39,14 @@
"sha256": "6fdc15530c75676e46e3e44b6213017287d19ddc9c71597493b346072d1ee67c"
}
],
- "evidence_path": "runs/pr51-static-replacement-20261015/assembled-evidence",
+ "evidence_path": "runs/pr51-static-replacement-20261015/review-followup-final-evidence",
"task_replacement": {
"path": "local-evidence/task-replacement/receipt.json",
- "sha256": "fddebdd3e5e5a554fc5e52fbfc806affae7ac77962f5e02b6181f140254af2ee"
+ "sha256": "c68d3b21c3317538089635f8fb0ca5bf4f874f4a03201b27e42c56491780d6e0"
},
"offline_revision": {
- "path": "local-evidence/offline-revisions/6d78a8bc3fd58900/receipt.json",
- "sha256": "6d3a10832c6e5136d4d01a170c5f6fbd05922b836cab8f8acc22065a394c9ada"
+ "path": "local-evidence/offline-revisions/6d78a8bc3fd58900-8d5edbea48da0319/receipt.json",
+ "sha256": "a11c53aae6635ff0915b1e2be1bad7f489ee72c83e2bbb95deddfb38bcd31635"
},
"replacement_case": {
"id": "static-sqlite-quickstart",
@@ -83,17 +83,29 @@
]
}
],
- "forbidden_any": [
- "Page Not Found",
- "Access denied",
- "CAPTCHA"
- ],
"body_coverage": {
"version": 3,
"minimum_coverage": 0.8,
"minimum_floor": 0.6,
"minimum_region_passes": 4,
"regions": 5
+ },
+ "diagnostic_any": [
+ "Page Not Found",
+ "Access denied",
+ "CAPTCHA"
+ ]
+ },
+ "current_scoring_source_sha256": "d4ec10f02e742d6b94d3e9e2aa8a1d605ab3954f59bfd7908a096fc5a4958916",
+ "review_evidence_package": {
+ "url": "https://github.com/lexmount/browser-eval/releases/download/untagged-c84db0e3f874d633407c/PR-51-current-rule-and-sqlite-evidence.zip",
+ "sha256": "3218b22ad388ebb2ae3e084e9378d323ddd9950e75783ae33f75339a4812bdc0",
+ "bytes": 14815846,
+ "access": "GitHub draft release; repository write permission required",
+ "prerequisite": {
+ "url": "https://github.com/lexmount/browser-eval/releases/download/untagged-c84db0e3f874d633407c/PR-51-diagnostic-correction-patch.zip",
+ "sha256": "90d51b8bed3c8c480a45160a3cd8d624753451ee08f04ba816b7e6bb708d1c24",
+ "bytes": 7580469
}
}
}
diff --git a/tests/test_adjudication_evidence.py b/tests/test_adjudication_evidence.py
index 8389026bc..438eb4741 100644
--- a/tests/test_adjudication_evidence.py
+++ b/tests/test_adjudication_evidence.py
@@ -24,3 +24,48 @@ def check():
row['adjudication_evidence']['samples'][0]['text'] = 'invented evidence'
with pytest.raises(ValueError, match='excerpt differs'):
check()
+
+
+@pytest.mark.parametrize('defect', [None, 'opinion', 'verdict', 'excerpt', 'target_rule', 'missing_receipt'])
+def test_diagnostic_field_migration_preserves_historical_review(tmp_path, defect):
+ import copy
+ import json
+ from harness.web_fetch.adjudication_evidence import _require_review_binding_revision
+ raw = b'Required body. CAPTCHA'
+ content_hash = hashlib.sha256(raw).hexdigest()
+ old = {'id': 'a', 'required_facts': [{'id': 'body', 'alternatives': ['Required body']}],
+ 'forbidden_any': ['CAPTCHA']}
+ current = {k: v for k, v in old.items() if k != 'forbidden_any'}
+ current['diagnostic_any'] = ['CAPTCHA']
+ row = {'attempt_id': 'p__a', 'case_id': 'a', 'content_sha256': content_hash,
+ 'agent_verdict': 'fail', 'current_verdict': 'pass', 'final_verdict': 'pass',
+ 'adjudication': 'Required body is present; the word is diagnostic.',
+ 'adjudication_evidence': {'case_sha256': case_hash(old), 'rule_ids': ['required_facts', 'forbidden_any'],
+ 'content_sha256': content_hash, 'samples': [{'text': 'Required body'}]}}
+ def save(name, value):
+ path = tmp_path/name
+ path.write_text(json.dumps(value))
+ return {'path': name, 'sha256': hashlib.sha256(path.read_bytes()).hexdigest()}
+ old_review = save('old-review.json', {'results_sha256': 'a'*64, 'attempts': [row]})
+ old_dataset = save('old-dataset.json', {'cases': [old]})
+ receipt = save('score-revision.json', {'rule_revision': True, 'provider_calls_added': 0,
+ 'authority': {'preserved_render_input_results_sha256': 'a'*64}, 'input_dataset': old_dataset})
+ migrated = copy.deepcopy(row)
+ proof = migrated['adjudication_evidence']
+ proof['prior_case_sha256'] = proof['case_sha256']
+ proof['case_sha256'] = case_hash(current)
+ proof['rule_ids'] = ['required_facts', 'diagnostic_any']
+ review = {'attempts': [migrated], 'rule_binding_revision': {
+ 'kind': 'unchanged_target_and_verdict_evidence_carryforward', 'new_independent_review': False,
+ 'prior_review': old_review, 'score_revision': receipt, 'attempt_ids': ['p__a']}}
+ if defect == 'opinion': migrated['agent_verdict'] = 'pass'
+ elif defect == 'verdict': migrated['final_verdict'] = 'fail'
+ elif defect == 'excerpt': proof['samples'][0]['text'] = 'Invented body'
+ elif defect == 'target_rule': current['required_facts'] = []
+ elif defect == 'missing_receipt': review.pop('rule_binding_revision')
+ records = [{'attempt_id': 'p__a', 'metrics': {'content_sha256': content_hash}}]
+ if defect:
+ with pytest.raises(ValueError):
+ _require_review_binding_revision(tmp_path, {'cases': [current]}, records, review)
+ else:
+ _require_review_binding_revision(tmp_path, {'cases': [current]}, records, review)
diff --git a/tests/test_anomaly_review.py b/tests/test_anomaly_review.py
index 6edb97832..5dc6c7a27 100644
--- a/tests/test_anomaly_review.py
+++ b/tests/test_anomaly_review.py
@@ -44,7 +44,7 @@ def test_real_stable_structure_forbidden_shape_triggers(self):
'provider':'moli-webfetch','case_id':'dynamic-react-cart','ok':True,
'artifact_path':'answer.txt','metrics':{
'stable_structure_pass':True, 'stable_content_pass':False,
- 'stable_task_pass':False, 'forbidden_hits':['Add to cart'],
+ 'stable_task_pass':False, 'forbidden_requirement_hits':['Add to cart'],
'stable_task_errors':['forbidden_content']}}
self.assertIn('positive-structure-only-forbidden-failed',
detect_anomalies(root,[row])[row['attempt_id']])
@@ -96,7 +96,7 @@ def add(case, text='', *, ok=True, error=None, status=None, route='ordinary_mark
metrics['stable_complete_record_count']=complete
if forbidden:
metrics.update(stable_structure_pass=True,
- forbidden_hits=forbidden,
+ forbidden_requirement_hits=forbidden,
stable_task_errors=['forbidden_content'])
rows.append({'attempt_id':f'moli-webfetch__{case}__r001',
'provider':'moli-webfetch','case_id':case,'ok':ok,
diff --git a/tests/test_dynamic_100_moli_119.py b/tests/test_dynamic_100_moli_119.py
index fc1df4a33..c4a45ab1f 100644
--- a/tests/test_dynamic_100_moli_119.py
+++ b/tests/test_dynamic_100_moli_119.py
@@ -8,7 +8,6 @@
from harness.web_fetch.reference_inputs import input_tree
from harness.web_fetch.report import local_audit_banner, provider_configuration
from harness.web_fetch.storage import data_home
-from harness.web_fetch.scoring_regressions import load_scoring_regressions
from harness.web_fetch.stable_tasks import validate_acceptance_contract
from tests.conftest import read_frozen_dataset_bytes
@@ -53,7 +52,6 @@ def test_dynamic_100_dataset_matches_the_frozen_local_audit(pytestconfig):
assert historical.count(line)==1
historical=historical.replace(line,'',1)
assert hashlib.sha256(historical.encode()).hexdigest()==followup['predecessor_sha256']
- controls = load_scoring_regressions()
for change in revision["dynamic_dataset"]["changes"]:
case = next(case for case in cases if case["id"] == change["case_id"])
assert case[change["field"]] == change["value"]
@@ -92,6 +90,8 @@ def test_dynamic_100_dataset_matches_the_frozen_local_audit(pytestconfig):
(snapshot / "harness/web_fetch" / name).read_bytes() for name in scorer_files)).hexdigest()
assert scorer_revision["scoring_regressions_sha256"] == hashlib.sha256(
(snapshot / "evaluation/scoring-regressions.json").read_bytes()).hexdigest()
+ controls = {row['id']: row for row in json.loads(
+ (snapshot / 'evaluation/scoring-regressions.json').read_text())['cases']}
assert set(scorer_revision["regression_ids"]) <= set(controls)
modes = {}
for identifier in scorer_revision["regression_ids"]:
@@ -136,6 +136,18 @@ def test_dynamic_100_dataset_matches_the_frozen_local_audit(pytestconfig):
assert dataset["comparison"]["local_audit_receipt"] == RECEIPT_PATH.relative_to(ROOT).as_posix()
+def historical_controls_path():
+ revision = json.loads(REVISION_PATH.read_text())
+ scorer = revision['shared_scorer']
+ for followup in revision.get('shared_scorer_followups', []):
+ scorer = {**scorer, **followup}
+ path = data_home() / 'scoring-snapshots' / scorer['revised_sha256'] / 'evaluation/scoring-regressions.json'
+ if not path.is_file():
+ pytest.skip('requires externally installed frozen historical scorer inputs')
+ assert hashlib.sha256(path.read_bytes()).hexdigest() == scorer['scoring_regressions_sha256']
+ return path
+
+
def require_historical_inputs():
try:
input_tree(Path('evaluation/dataset.json'), 'sources')
@@ -158,7 +170,7 @@ def test_historical_scoring_controls_allow_only_new_tail_entries(tmp_path, monke
require_historical_inputs()
dataset = _dataset()
receipt = json.loads(RECEIPT_PATH.read_text(encoding="utf-8"))
- registry = json.loads((ROOT / "evaluation/scoring-regressions.json").read_text())
+ registry = json.loads(historical_controls_path().read_text())
original_file = local_audit._repository_file
replacement = tmp_path / "scoring-regressions.json"
@@ -228,10 +240,14 @@ def test_local_audit_banner_depends_on_dataset_publication_state():
assert "本地审核版" in local_audit_banner(_dataset())
-def test_dynamic_100_repository_evidence_and_scoring_revision_are_exact():
+def test_dynamic_100_repository_evidence_and_scoring_revision_are_exact(monkeypatch):
require_historical_inputs()
dataset = _dataset()
receipt = json.loads(RECEIPT_PATH.read_text(encoding="utf-8"))
+ original = local_audit._repository_file
+ frozen = historical_controls_path()
+ monkeypatch.setattr(local_audit, '_repository_file', lambda relative:
+ frozen if relative == 'evaluation/scoring-regressions.json' else original(relative))
local_audit._require_repository_bindings(dataset, receipt)
diff --git a/tests/test_failure_diagnosis.py b/tests/test_failure_diagnosis.py
index cd5f4cfc0..6f67a036a 100644
--- a/tests/test_failure_diagnosis.py
+++ b/tests/test_failure_diagnosis.py
@@ -110,13 +110,13 @@ def test_prompt_diagnostics_do_not_replace_missing_target_reason(self):
self.assertIsNotNone(result)
self.assertNotIn('blocked_page', result['evidence'])
- def test_qualified_content_with_forbidden_hint_has_no_failure(self):
+ def test_qualified_content_with_diagnostic_hint_has_no_failure(self):
facts = [{'id': 'target', 'alternatives': ['Required target content']}]
metrics = evaluate_content('Required target content\nAccess denied', [], required_facts=facts,
- forbidden_any=['Access denied'])
- self.assertEqual(metrics['forbidden_hits'], ['Access denied'])
+ diagnostic_any=['Access denied'])
+ self.assertEqual(metrics['diagnostic_hits'], ['Access denied'])
self.assertIsNone(diagnose(self.record(metrics), {'task_kind': 'body'}, ''))
- missing = evaluate_content('Access denied', [], required_facts=facts, forbidden_any=['Access denied'])
+ missing = evaluate_content('Access denied', [], required_facts=facts, diagnostic_any=['Access denied'])
self.assertFalse(missing['content_pass'])
self.assertIsNotNone(diagnose(self.record(missing), {'task_kind': 'body'}, ''))
diff --git a/tests/test_fairness.py b/tests/test_fairness.py
index 2f249a78b..457eb3751 100644
--- a/tests/test_fairness.py
+++ b/tests/test_fairness.py
@@ -304,22 +304,23 @@ def test_dynamic_prompt_hits_are_diagnostic_when_target_is_complete(self):
],
}
for identifier, tokens in supplemental.items():
- case = cases[identifier]
+ case = dict(cases[identifier])
+ case['diagnostic_any'] = case.pop('forbidden_any', [])
target_only = case['ground_truth_main_content']
clean = score_attempt(case, target_only.encode(), ok=True)
with self.subTest(case=identifier, variant='target-only'):
self.assertTrue(task_passed(True, clean))
- self.assertEqual(clean['forbidden_hits'], [])
+ self.assertEqual(clean['diagnostic_hits'], [])
for token in tokens:
extended = score_attempt(
case, f'{target_only}\n\n{token}'.encode(), ok=True)
with self.subTest(case=identifier, supplemental=token):
self.assertTrue(extended['stable_structure_pass'])
if identifier == 'dynamic-react-cart' or token == 'Similar Products':
- self.assertEqual(extended['forbidden_hits'], [token])
+ self.assertEqual(extended['diagnostic_hits'], [token])
self.assertTrue(task_passed(True, extended))
else:
- self.assertEqual(extended['forbidden_hits'], [])
+ self.assertEqual(extended['diagnostic_hits'], [])
self.assertTrue(task_passed(True, extended))
def test_end_to_end_processing_is_timed_and_late_answers_rejected(self):
diff --git a/tests/test_local_audit.py b/tests/test_local_audit.py
index cb1171784..8dbc3d40e 100644
--- a/tests/test_local_audit.py
+++ b/tests/test_local_audit.py
@@ -24,6 +24,21 @@ def test_frozen_scoring_controls_allow_additions_but_reject_edits(tmp_path, monk
scoring_path.parent.mkdir(parents=True)
scoring_path.write_bytes((original_root / bindings['scoring_revision_path']).read_bytes())
scoring = json.loads(scoring_path.read_text())
+ # This boundary test owns a frozen synthetic registry. Historical measured
+ # registries are retained with their scorer, not selected by today's runtime.
+ controls = {'cases': [
+ {'id': 'base', 'artifact': 'fixed'},
+ {'id': 'positive', 'artifact': 'complete', 'root_cause': 'fixture', 'failure_mode': 'false-negative'},
+ {'id': 'negative', 'artifact': 'missing', 'root_cause': 'fixture', 'failure_mode': 'false-positive'}]}
+ scoring['preexisting_control_ids'] = ['base']
+ scoring['revision_controls'] = [{k: row[k] for k in ('id', 'root_cause', 'failure_mode')}
+ for row in controls['cases'][1:]]
+ scoring['bindings']['preexisting_controls_sha256'] = local_audit._canonical_sha256(controls['cases'][:1])
+ scoring['bindings']['revision_controls_sha256'] = local_audit._canonical_sha256(controls['cases'][1:])
+ scoring['root_causes'] = [{'id': 'fixture', 'false_positive_ids': ['negative'], 'false_negative_ids': ['positive']}]
+ scoring['scope'] = {'preexisting_controls': 1, 'revision_controls': 2, 'root_causes': 1, 'paired_error_directions': True}
+ _write(scoring_path, scoring)
+ bindings['scoring_revision_sha256'] = _digest(scoring_path)
dataset = {'id': scoring['dataset_id'], 'cases': [{'id': 'fixture'}]}
reference = root / 'evaluation/sources/fixture/reference.txt'
reference.parent.mkdir(parents=True)
@@ -42,7 +57,6 @@ def test_frozen_scoring_controls_allow_additions_but_reject_edits(tmp_path, monk
})
bindings['source_manifest_sha256'] = _digest(source_manifest)
relative = 'evaluation/scoring-regressions.json'
- controls = json.loads((original_root / relative).read_text())
controls['cases'].append({'id': 'new-independent-control'})
path = root / relative
_write(path, controls)
@@ -719,6 +733,13 @@ def test_retrospective_page_review_preserves_call_and_checks_other_requirements(
assert record == before and record['final_url'] is None
assert task_passed(True, scored)
assert scored['page_identity_basis'] == 'retrospective_content_correspondence'
+ # Two substantial matches establish correspondence; additional true short
+ # labels do not invalidate them and cannot replace either long match.
+ with_short = {**annotation, 'matching_samples': annotation['matching_samples'] + ['body']}
+ assert task_passed(True, local_audit.score_reviewed_page_identity(tmp_path, case, record, with_short, captures))
+ with pytest.raises(ValueError, match='substantive text'):
+ local_audit.score_reviewed_page_identity(tmp_path, case, record,
+ {**annotation, 'matching_samples': [annotation['matching_samples'][0], 'body']}, captures)
for samples in ([' ' * 20, ' ' * 21], ['.' * 20, '!' * 21],
['Expected complete body.', '**Expected complete body.**']):
with pytest.raises(ValueError, match='substantive text'):
@@ -751,3 +772,50 @@ def test_retrospective_page_review_requires_bound_receipt(tmp_path):
local_audit._require_page_identity_review(tmp_path, {}, [
{'attempt_id': 'synthetic', 'metrics': {
'page_identity_basis': 'retrospective_content_correspondence'}}])
+
+
+@pytest.mark.parametrize('defect', [None, 'target_changed', 'sample_changed'])
+def test_offline_hint_revision_reapplies_bound_identity_evidence(tmp_path, defect):
+ import copy
+ from harness.web_fetch.scoring import score_attempt
+ body = 'Expected complete body. A second distinctive sentence describing this page.'
+ old = {'id': 'synthetic', 'url': 'https://example.test/start', 'track': 'risk_control',
+ 'task_kind': 'body', 'forbidden_any': ['CAPTCHA'],
+ 'source_benchmark': {'risk_control': {'navigation_contract': {
+ 'requested_url': 'https://example.test/start', 'observed_final_url': 'https://example.test/document'}}},
+ 'acceptance_contract': {'version': 1, 'schema': 'body_text', 'minimum_records': 0,
+ 'stable_text_anchors': [{'id': 'body', 'alternatives': ['Expected complete body']}]}}
+ current = copy.deepcopy(old)
+ current['diagnostic_any'] = current.pop('forbidden_any')
+ (tmp_path/'answer.txt').write_text(body)
+ record = {'attempt_id': 'method__synthetic__r001', 'case_id': 'synthetic',
+ 'artifact_path': 'answer.txt', 'ok': True, 'final_url': None,
+ 'metrics': score_attempt(current, body.encode(), ok=True)}
+ captures = [{'url': 'https://example.test/document', 'text': body}]
+ def save(name, obj):
+ _write(tmp_path/name, obj)
+ return {'path': name, 'sha256': _digest(tmp_path/name)}
+ capture = save('captures.json', captures)
+ annotation = {'attempt_id': record['attempt_id'], 'case_sha256': local_audit._canonical_sha256(old),
+ 'call_sha256': local_audit.local_measurement_call_sha256(record),
+ 'content_sha256': record['metrics']['content_sha256'], 'basis': 'retrospective_content_correspondence',
+ 'reviewed_at': '2026-10-13', 'capture_index': 0,
+ 'capture_sha256': local_audit._canonical_sha256(captures[0]), 'corresponding_url': captures[0]['url'],
+ 'matching_samples': ['Expected complete body.', 'A second distinctive sentence describing this page.']}
+ if defect == 'sample_changed': annotation['matching_samples'] = [' '*20, ' '*21]
+ receipt = save('identity.json', {'kind': 'retrospective_page_identity_review',
+ 'reviewed_at': '2026-10-13', 'basis': 'retained human review', 'provider_calls_added': 0,
+ 'captures_path': capture['path'], 'captures_sha256': capture['sha256'], 'attempts': [annotation]})
+ before = {'cases': [old], 'comparison': {'page_identity_review': receipt}}
+ dataset = {'cases': [current], 'comparison': {}}
+ prior_receipt = (tmp_path/'identity.json').read_bytes()
+ if defect == 'target_changed': current['task_prompt'] = 'A new target requirement'
+ if defect:
+ with pytest.raises(ValueError):
+ local_audit.carry_reviewed_page_identities(tmp_path, before, dataset, [record])
+ else:
+ local_audit.carry_reviewed_page_identities(tmp_path, before, dataset, [record])
+ assert local_audit.task_passed(record['ok'], record['metrics'])
+ assert record['final_url'] is None
+ local_audit._require_page_identity_review(tmp_path, dataset, [record])
+ assert (tmp_path/'identity.json').read_bytes() == prior_receipt
diff --git a/tests/test_local_diagnostic_presentation.py b/tests/test_local_diagnostic_presentation.py
index 7374726c1..cf1cd974b 100644
--- a/tests/test_local_diagnostic_presentation.py
+++ b/tests/test_local_diagnostic_presentation.py
@@ -136,7 +136,8 @@ def test_explicit_sample_ranking_reuses_existing_ranked_table_and_keeps_scope(tm
assert 'z-high' in soup.select_one('tbody tr').get_text()
banner = local_audit_banner(dataset)
assert 'data-ranking-scope="saved-task-pass-rate"' in banner
- assert BeautifulSoup(banner, 'html.parser').get_text() == ''
+ assert '未完成仓库认证流程' in BeautifulSoup(banner, 'html.parser').get_text()
+ assert 'hidden' not in banner
assert 'callout' not in banner
page = tmp_path/'index.html'
page.write_text('
'+banner+markup)
@@ -172,3 +173,16 @@ def test_contract_prose_localizes_without_full_snapshot_or_fixed_field_counts(tr
parser.feed(english)
assert parser.untranslated == []
assert 'shipping: at least 2 records' in english
+
+
+@pytest.mark.parametrize('wrapper', [
+ '
Uncertified',
+ '
Uncertified
',
+ '
',
+ '
'])
+def test_sample_ranking_rejects_invisible_scope_disclosure(tmp_path, wrapper):
+ from harness.web_fetch.report_check import check_local_diagnostic_presentation
+ attrs = 'data-evidence-state="local_measurement" data-ranking-scope="saved-task-pass-rate"'
+ (tmp_path/'index.html').write_text('
'+wrapper.format(attrs=attrs))
+ with pytest.raises(ValueError, match='disclosure missing'):
+ check_local_diagnostic_presentation(tmp_path, ranked=True)
diff --git a/tests/test_local_revision.py b/tests/test_local_revision.py
index 642e588cf..dbb36397c 100644
--- a/tests/test_local_revision.py
+++ b/tests/test_local_revision.py
@@ -54,30 +54,53 @@ def test_offline_revision_preserves_calls_and_rejects_tampering(tmp_path):
require_local_revision(tmp_path, binding, dataset, rows)
-def test_policy_revision_records_version_and_rejects_parser_only_label(tmp_path):
+def test_contract_revision_is_explicit_without_runtime_policy_switch(tmp_path):
raw = b'Required body\nCAPTCHA'
- (tmp_path / 'answer.txt').write_bytes(raw)
- case = {'id': 'a', 'url': 'https://example.test/', 'task_kind': 'body', 'acceptance_policy_version': 1,
- 'forbidden_any': ['CAPTCHA'], 'acceptance_contract': {'version': 1, 'schema': 'body_text',
- 'minimum_records': 0, 'stable_text_anchors': [{'id': 'body', 'alternatives': ['Required body']}]}}
+ (tmp_path/'answer.txt').write_bytes(raw)
+ case = {'id': 'a', 'url': 'https://example.test/', 'task_kind': 'body',
+ 'forbidden_any': ['CAPTCHA'], 'required_facts': [
+ {'id': 'body', 'alternatives': ['Required body']}]}
original = {'attempt_id': 'p__a', 'provider': 'p', 'case_id': 'a', 'ok': True,
'artifact_path': 'answer.txt', 'metrics': score_attempt(case, raw, ok=True)}
assert not task_passed(True, original['metrics'])
- (tmp_path / 'dataset.json').write_text(json.dumps({'cases': [case], 'comparison': {}}))
- (tmp_path / 'results.jsonl').write_text(json.dumps(original)+'\n')
- config = tmp_path / 'config.json'
- config.write_text(json.dumps({'cases': [{**case, 'acceptance_policy_version': 2}], 'rule_revision': True,
- 'authority': {'description': 'Explicitly adopt policy 2; verification words are diagnostics'},
- 'regression_ids': ['html-doctype-visible-challenge-policy-v2-diagnostic', 'prompt-diagnostic-only-verification']}))
+ (tmp_path/'dataset.json').write_text(json.dumps({'cases': [case], 'comparison': {}}))
+ (tmp_path/'results.jsonl').write_text(json.dumps(original)+'\n')
+ changed = {k: v for k, v in case.items() if k != 'forbidden_any'}
+ changed['diagnostic_any'] = ['CAPTCHA']
+ config = tmp_path/'config.json'
+ config.write_text(json.dumps({'cases': [changed], 'rule_revision': True,
+ 'authority': {'description': 'Explicit task revision: error words are diagnostic observations'},
+ 'regression_ids': ['declared-forbidden-token-fails', 'unforbidden-captcha-documentation-passes']}))
receipt = revise_local_snapshot(tmp_path, config)
- assert receipt['rule_revision'] is True and receipt['acceptance_policy_version'] == 2
- rows = [json.loads((tmp_path / 'results.jsonl').read_text())]
+ assert receipt['rule_revision'] is True
+ assert 'acceptance_policy_version' not in receipt
+ rows = [json.loads((tmp_path/'results.jsonl').read_text())]
+ assert 'acceptance_policy_version' not in rows[0]['metrics']
assert task_passed(True, rows[0]['metrics'])
- rows[0]['scoring']['rule_revision'] = False
- (tmp_path / 'results.jsonl').write_text(json.dumps(rows[0])+'\n')
- path = next((tmp_path / 'local-evidence/offline-revisions').glob('*/receipt.json'))
- receipt['output_results_sha256'] = hashlib.sha256((tmp_path / 'results.jsonl').read_bytes()).hexdigest()
- path.write_text(json.dumps(receipt))
- binding = {'path': str(path.relative_to(tmp_path)), 'sha256': hashlib.sha256(path.read_bytes()).hexdigest()}
- with pytest.raises(ValueError, match='parser-only'):
- require_local_revision(tmp_path, binding, json.loads((tmp_path / 'dataset.json').read_text()), rows)
+
+
+def test_same_input_keeps_distinct_revision_receipts(tmp_path):
+ raw = b'Required article text. CAPTCHA'
+ (tmp_path/'answer.txt').write_bytes(raw)
+ case = {'id': 'a', 'url': 'https://example.test/', 'required_facts': [
+ {'id': 'body', 'alternatives': ['Required article text']}], 'forbidden_any': ['CAPTCHA']}
+ row = {'attempt_id': 'p__a', 'provider': 'p', 'case_id': 'a', 'ok': True,
+ 'artifact_path': 'answer.txt', 'metrics': score_attempt(case, raw, ok=True)}
+ original_dataset = json.dumps({'cases': [case], 'comparison': {}})
+ original_results = json.dumps(row)+'\n'
+ changed = {k: v for k, v in case.items() if k != 'forbidden_any'}
+ changed['diagnostic_any'] = ['CAPTCHA']
+ config = tmp_path/'config.json'
+ preserved = None
+ for reason in ['First declared correction', 'Second explicitly distinct revision']:
+ (tmp_path/'dataset.json').write_text(original_dataset)
+ (tmp_path/'results.jsonl').write_text(original_results)
+ config.write_text(json.dumps({'cases': [changed], 'rule_revision': True,
+ 'authority': {'description': reason},
+ 'regression_ids': ['declared-forbidden-token-fails', 'unforbidden-captcha-documentation-passes']}))
+ receipt = revise_local_snapshot(tmp_path, config)
+ path = tmp_path/receipt['input_dataset']['path']
+ if preserved is None:
+ preserved = (path.parent/'receipt.json', (path.parent/'receipt.json').read_bytes())
+ assert len(list((tmp_path/'local-evidence/offline-revisions').glob('*/receipt.json'))) == 2
+ assert preserved[0].read_bytes() == preserved[1]
diff --git a/tests/test_local_task_replacement.py b/tests/test_local_task_replacement.py
index 4e92acf04..8a49eb7d9 100644
--- a/tests/test_local_task_replacement.py
+++ b/tests/test_local_task_replacement.py
@@ -9,25 +9,62 @@
def setup_replacement(tmp_path):
+ from datetime import datetime, timezone
+ from harness.web_fetch.config import load_dataset
+ from harness.web_fetch.providers import build_provider
+ from harness.web_fetch.runner import run_evaluation
before = {'cases': [{'id': 'old', 'track': 'web'}, {'id': 'keep', 'track': 'web'}],
'coverage_plan': {'web': ['a', 'b']}}
- added = {'id': 'new', 'track': 'web'}
+ added = {'id': 'new', 'name': 'New fixture', 'track': 'web', 'category': 'fixture',
+ 'url': 'https://example.invalid/', 'expected_any': ['fixture']}
dataset = {**before, 'cases': [added, before['cases'][1]]}
def row(p, c):
return {'attempt_id': p+'__'+c, 'provider': p, 'case_id': c,
'run': 1, 'metrics': {'scoring_source_sha256': 'scorer-'+c}}
previous = [row(p, c) for p in ['a', 'b'] for c in ['old', 'keep']]
- calls = [row(p, 'new') for p in ['a', 'b']]
- receipt = {'kind': 'local_task_replacement', 'reason': 'private DNS address',
- 'removed_case_id': 'old', 'added_case_id': 'new', 'provider_calls_added': 2}
def save(name, data, lines=False):
raw = (''.join(json.dumps(r)+'\n' for r in data) if lines else json.dumps(data)).encode()
(tmp_path/name).write_bytes(raw)
return {'path': name, 'sha256': hashlib.sha256(raw).hexdigest()}
+ input_binding = save('frozen.json', {'tool': 'web_fetch', 'cases': [added]})
+ providers = save('providers.json', {'providers': {p: {'type': 'fixture'} for p in ['a', 'b']}})
+ network = save('network.json', {'dns_mode': 'system', 'hosts': [], 'usable_for_guarded_local_comparison': True})
+ source = save('source.html', '
fixture
')
+ reference = save('reference.txt', 'fixture')
+ def capture(stage):
+ observation = save(stage+'.json', {'case_id': 'new', 'url': added['url'],
+ 'independent': True, 'status': 200, 'sha256': source['sha256'],
+ 'reference_sha256': reference['sha256'], 'extracted_sha256': reference['sha256'],
+ 'checked_at': datetime.now(timezone.utc).isoformat()})
+ return {'evidence': observation, 'source': source, 'reference': reference, 'extracted': reference}
+ pre = capture('pre')
+ _, cases = load_dataset(tmp_path/'frozen.json')
+ calls, batches = [], []
+ for method in ['a', 'b']:
+ rows, _ = run_evaluation(output_dir=tmp_path,
+ dataset_meta={'tool': 'web_fetch', 'cases': [added]}, dataset_path=tmp_path/'frozen.json',
+ cases=cases, providers=[build_provider(method, {'type': 'fixture'})],
+ provider_config_path=tmp_path/'providers.json', runs=1, timeout=45, delay=0)
+ batch = json.loads((tmp_path/'manifest.json').read_text())
+ calls.extend(rows)
+ batches.append(batch)
+ post = capture('post')
+ contract = {'schema_version': 1, 'kind': 'uniform_native_local_measurement',
+ 'providers': ['a', 'b'], 'runs': 1, 'timeout_seconds': 45, 'max_failure_retries': 0,
+ 'dataset_storage': 'evidence', 'dataset_sources': [input_binding],
+ 'provider_config_sha256': providers['sha256'],
+ 'runtime_source_sha256': batches[0]['runtime_source_sha256'],
+ 'environment': batches[0]['environment'], 'parameters': batches[0]['parameters'],
+ 'runner_inputs': {'dataset': input_binding, 'providers': providers, 'network': network,
+ 'source_observations': {'new': {'pre': pre, 'post': post}}}}
+ receipt = {'kind': 'local_task_replacement', 'reason': 'private DNS address',
+ 'removed_case_id': 'old', 'added_case_id': 'new', 'provider_calls_added': 2}
receipt['baseline_dataset'] = save('before.json', before)
receipt['baseline_results'] = save('previous.jsonl', previous, True)
receipt['cohort_dataset'] = save('cohort.json', {'cases': [added]})
receipt['cohort_results'] = save('calls.jsonl', calls, True)
+ receipt['execution_contract'] = save('execution-contract.json', contract)
+ receipt['cohort_execution'] = save('execution.json', {'batches': batches})
binding = save('receipt.json', receipt)
records = [r for r in previous if r['case_id'] != 'old']+calls
return binding, dataset, records
@@ -60,12 +97,12 @@ def test_replacement_execution_preserves_history_without_counting_old_task(tmp_p
from harness.web_fetch.report_check import require_replacement_execution
binding, dataset, records = setup_replacement(tmp_path)
physical = Counter({'a__old': 1, 'b__old': 1, 'a__keep': 1, 'b__keep': 1,
- 'a__new': 1, 'b__new': 1})
+ 'a__new__r001': 1, 'b__new__r001': 1})
logical = Counter(r['attempt_id'] for r in records)
if defect == 'missing_history':
del physical['a__old']
elif defect == 'extra_call':
- physical['a__new'] += 1
+ physical['a__new__r001'] += 1
elif defect == 'old_task_counted':
logical['a__old'] = 1
if defect:
@@ -73,3 +110,31 @@ def test_replacement_execution_preserves_history_without_counting_old_task(tmp_p
require_replacement_execution(tmp_path, binding, physical, logical)
else:
require_replacement_execution(tmp_path, binding, physical, logical)
+
+
+@pytest.mark.parametrize('defect', ['missing_contract', 'missing_inputs', 'environment', 'timeout',
+ 'config', 'dns', 'source', 'late_precheck', 'output'])
+def test_replacement_requires_full_physical_execution_evidence(tmp_path, defect):
+ binding, dataset, records = setup_replacement(tmp_path)
+ receipt = json.loads((tmp_path/'receipt.json').read_text())
+ if defect == 'missing_contract':
+ receipt.pop('execution_contract')
+ elif defect in {'missing_inputs', 'environment', 'timeout', 'config'}:
+ contract = json.loads((tmp_path/'execution-contract.json').read_text())
+ if defect == 'missing_inputs': contract.pop('runner_inputs')
+ elif defect == 'environment': contract['environment']['python'] = 'wrong'
+ elif defect == 'timeout': contract['timeout_seconds'] = 44
+ else: contract['provider_config_sha256'] = '0'*64
+ (tmp_path/'execution-contract.json').write_text(json.dumps(contract))
+ receipt['execution_contract']['sha256'] = hashlib.sha256((tmp_path/'execution-contract.json').read_bytes()).hexdigest()
+ elif defect == 'dns': (tmp_path/'network.json').write_text('{}')
+ elif defect == 'source': (tmp_path/'source.html').write_text('wrong page')
+ elif defect == 'late_precheck':
+ observation = json.loads((tmp_path/'pre.json').read_text())
+ observation['checked_at'] = '2099-01-01T00:00:00+00:00'
+ (tmp_path/'pre.json').write_text(json.dumps(observation))
+ else: (tmp_path/records[-1]['artifact_path']).write_text('changed output')
+ (tmp_path/'receipt.json').write_text(json.dumps(receipt))
+ binding['sha256'] = hashlib.sha256((tmp_path/'receipt.json').read_bytes()).hexdigest()
+ with pytest.raises(ValueError):
+ require_local_task_replacement(tmp_path, binding, dataset, records)
diff --git a/tests/test_metrics.py b/tests/test_metrics.py
index 420a21eec..aa4ed1271 100644
--- a/tests/test_metrics.py
+++ b/tests/test_metrics.py
@@ -133,10 +133,10 @@ def test_ground_truth_requires_every_fact_and_keeps_prompt_diagnostics(self) ->
self.assertEqual(partial["fact_coverage"], 0.5)
blocked = evaluate_content(
"Example Domain documentation examples Access denied",
- ["Example Domain"], required_facts=facts, forbidden_any=["Access denied"],
+ ["Example Domain"], required_facts=facts, diagnostic_any=["Access denied"],
)
self.assertTrue(blocked["content_pass"])
- self.assertEqual(blocked["forbidden_hits"], ["Access denied"])
+ self.assertEqual(blocked["diagnostic_hits"], ["Access denied"])
def test_summary_separates_transport_and_content(self) -> None:
base = {
diff --git a/tests/test_report_article_cases.py b/tests/test_report_article_cases.py
index cdd9c83ea..46ee030ec 100644
--- a/tests/test_report_article_cases.py
+++ b/tests/test_report_article_cases.py
@@ -41,7 +41,7 @@ def test_text_evidence_is_escaped_without_rewriting_its_content(self):
self.assertIn('<script>', left)
self.assertIn('<script>', right)
self.assertNotIn('
diff --git a/harness/web_fetch/providers/subprocess_jsonl.py b/harness/web_fetch/providers/subprocess_jsonl.py
index 8f76d664e..6ab6409cc 100644
--- a/harness/web_fetch/providers/subprocess_jsonl.py
+++ b/harness/web_fetch/providers/subprocess_jsonl.py
@@ -319,6 +319,9 @@ def fetch(self, case: EvalCase, timeout: float) -> ProviderResult:
error_message=None if ok else str(payload.get("error_message") or "worker failed"),
metadata={
**dict(payload.get("metadata") or {}),
+ **({'native_answer_projection': {'schema_version': 1,
+ 'content_pointer': ['content'], 'content_format': 'text'}}
+ if isinstance(payload.get('content'), str) else {}),
"_raw_response_b64": base64.b64encode(line.encode("utf-8")).decode("ascii"),
"raw_response_sha256": hashlib.sha256(line.encode("utf-8")).hexdigest(),
"raw_response_bytes": len(line.encode("utf-8")),
diff --git a/harness/web_fetch/response_input.py b/harness/web_fetch/response_input.py
index 7c0e8e8bd..cf1349a56 100644
--- a/harness/web_fetch/response_input.py
+++ b/harness/web_fetch/response_input.py
@@ -53,3 +53,87 @@ def safe_named_links(links, base_url):
if parts.scheme in {'http', 'https'} and parts.hostname and not parts.username and not parts.password:
result.append((url, item['title']))
return result
+
+
+def _projection_value(payload, pointer):
+ if not isinstance(pointer, list) or not pointer:
+ raise ValueError('Native answer projection requires a nonempty field pointer')
+ value = payload
+ try:
+ for key in pointer:
+ if isinstance(value, dict) and isinstance(key, str):
+ value = value[key]
+ elif isinstance(value, list) and type(key) is int and key >= 0:
+ value = value[key]
+ else:
+ raise ValueError('Native answer pointer is not a structural field selection')
+ except (KeyError, IndexError) as error:
+ raise ValueError('Native answer field is missing') from error
+ return value
+
+
+def _attribute_record_urls(groups, base_url):
+ """Project declared HTML URL attributes without classifying their targets."""
+ urls = []
+ for group in groups if isinstance(groups, list) else []:
+ if not isinstance(group, dict):
+ continue
+ elements = group.get('results')
+ for element in elements if isinstance(elements, list) else []:
+ if not isinstance(element, dict):
+ continue
+ attributes = element.get('attributes')
+ for attribute in attributes if isinstance(attributes, list) else []:
+ if not isinstance(attribute, dict):
+ continue
+ name, value = attribute.get('name'), attribute.get('value')
+ if not isinstance(name, str) or not isinstance(value, str):
+ continue
+ name = name.casefold()
+ if name not in {'src', 'srcset', 'href', 'data-src', 'data-original'}:
+ continue
+ values = ([part.strip().split()[0] for part in value.split(',') if part.strip()]
+ if name == 'srcset' else [value])
+ for raw in values:
+ try:
+ urls.append(urljoin(base_url, raw.strip()))
+ except ValueError:
+ # Retain malformed native values; the shared scorer,
+ # not a provider adapter, decides URL eligibility.
+ urls.append(raw)
+ return sorted(set(urls))
+
+
+def project_native_answer(payload, declaration):
+ """Select declared native fields; never inspect references or URL meaning."""
+ if declaration.get('schema_version') != 1:
+ raise ValueError('Unsupported native answer projection')
+ value = _projection_value(payload, declaration.get('content_pointer'))
+ kind = declaration.get('content_format')
+ if kind == 'text' and isinstance(value, str):
+ content = value
+ elif kind == 'json':
+ content = json.dumps(value, ensure_ascii=False, separators=(',', ':'))
+ else:
+ raise ValueError('Native answer field differs from its declared type')
+ fields = {}
+ for name, projection in declaration.get('answer_fields', {}).items():
+ if name not in {'links', 'images', 'videos', 'media', 'extras'}:
+ raise ValueError('Unknown native answer metadata field')
+ selected = _projection_value(payload, projection.get('pointer'))
+ if projection.get('format') == 'html_attribute_urls':
+ fields[name] = _attribute_record_urls(selected, projection['base_url'])
+ elif projection.get('format') == 'identity':
+ fields[name] = selected
+ else:
+ raise ValueError('Unknown native answer field projection')
+ return content, fields
+
+
+def require_native_answer_projection(content, metadata, raw_response):
+ declaration = metadata.get('native_answer_projection')
+ if declaration is None:
+ return
+ expected, fields = project_native_answer(json.loads(raw_response), declaration)
+ if content != expected or any(metadata.get(key) != value for key, value in fields.items()):
+ raise ValueError('Scored answer differs from its declared native field projection')
diff --git a/harness/web_fetch/runner.py b/harness/web_fetch/runner.py
index 60c69b696..710c217f4 100644
--- a/harness/web_fetch/runner.py
+++ b/harness/web_fetch/runner.py
@@ -379,6 +379,15 @@ def persist_result_artifacts(
output_dir: Path, artifact_rel: Path, result: ProviderResult
) -> Path:
"""Persist one physical call without changing its scored answer bytes."""
+ discarded_late_answer = (not result.ok and result.error_type == 'timeout'
+ and result.content == '')
+ if result.metadata.get('native_answer_projection') and not discarded_late_answer:
+ from .response_input import require_native_answer_projection
+ raw = result.metadata.get('_raw_response_b64')
+ if not isinstance(raw, str):
+ raise ValueError('Declared native answer lacks its retained raw response')
+ require_native_answer_projection(result.content, result.metadata,
+ base64.b64decode(raw, validate=True))
artifact = contained_path(output_dir, artifact_rel)
artifact.parent.mkdir(parents=True, exist_ok=True)
artifact.write_text(result.content, encoding='utf-8', errors='replace')
diff --git a/tests/test_browser_reference_scope.py b/tests/test_browser_reference_scope.py
index 5a6d9d3e1..0a0a8bb74 100644
--- a/tests/test_browser_reference_scope.py
+++ b/tests/test_browser_reference_scope.py
@@ -253,7 +253,8 @@ def test_resource_observation_matches_every_declared_target_and_cdn_variants():
'id': 'resource', 'url': 'https://example.test/page',
'track': 'image', 'task_kind': 'resource',
'expected_resources': [
- {'id': 'one', 'urls': ['https://images.unsplash.com/photo-one']},
+ {'id': 'one', 'urls': ['https://images.unsplash.com/photo-one'],
+ 'aliases': ['https://images.unsplash.com/photo-one?w=1200']},
{'id': 'two', 'urls': [], 'url_patterns': [r'^https://cdn\.example\.test/two(?:\?|$)']},
],
'resource_output_contract': {'unexpected': 'allow'},
diff --git a/tests/test_fixed_native_policy.py b/tests/test_fixed_native_policy.py
index e8cff000d..860a53f96 100644
--- a/tests/test_fixed_native_policy.py
+++ b/tests/test_fixed_native_policy.py
@@ -126,11 +126,20 @@ def test_all_430_frozen_cases_pass_native_input_guard():
assert count == 430
-def test_native_media_projection_keeps_returned_sources_and_video_links():
- from harness.web_fetch.providers.cloudflare import _resource_urls
- payload = {"result": [
- {"selector": "video", "results": [{"attributes": [{"name": "src", "value": "/a.mp4"}]}]},
- {"selector": "a[href]", "results": [{"attributes": [{"name": "href", "value": "/video/abc"}]}]}]}
- assert _resource_urls(payload, base_url="https://example.com/", track="video",
- include_video_page_links=True, only_video_page_links=False) == [
- "https://example.com/a.mp4", "https://example.com/video/abc"]
+def test_native_media_projection_keeps_all_returned_fields_without_link_guessing():
+ payload = {'success': True, 'result': [
+ {'selector': 'video', 'results': [{'attributes': [{'name': 'src', 'value': '/a.mp4'}]}]},
+ {'selector': 'a[href]', 'results': [{'attributes': [
+ {'name': 'href', 'value': '/opaque'}, {'name': 'href', 'value': '/about'},
+ {'name': 'title', 'value': 'Unchanged native label'}]}]}]}
+ case = EvalCase('control', 'control', 'https://example.com/', 'control', [],
+ track='video', task_kind='resource')
+ provider = build_provider('cf', {'type': 'cloudflare_markdown',
+ 'uniform_native_parameters': True, 'account_id': 'fixture', 'api_token': 'fixture'})
+ with patch('harness.web_fetch.providers.cloudflare.request',
+ return_value=HttpResponse(200, case.url, {}, json.dumps(payload).encode())) as call:
+ result = provider.fetch(case, 45)
+ assert call.call_count == 1
+ assert result.metadata['videos'] == [
+ 'https://example.com/a.mp4', 'https://example.com/about', 'https://example.com/opaque']
+ assert json.loads(result.content) == payload['result']
diff --git a/tests/test_metrics.py b/tests/test_metrics.py
index aa4ed1271..48114537a 100644
--- a/tests/test_metrics.py
+++ b/tests/test_metrics.py
@@ -298,7 +298,7 @@ def test_image_scope_ignores_ordinary_page_links(self) -> None:
self.assertEqual(result["resource_candidate_urls"], ["https://cdn.example.test/target.webp"])
self.assertEqual(result["unexpected_resource_urls"], [])
- def test_image_scope_accepts_declared_alias_pattern_and_normalized_thumbnail(self) -> None:
+ def test_image_scope_accepts_declared_alias_pattern_and_thumbnail_alias(self) -> None:
contract = {"candidate_scope": "image_answer_fields", "unexpected": "reject"}
alias = evaluate_resources(
"", {"images": ["/photo-480w.jpg"]},
@@ -318,6 +318,8 @@ def test_image_scope_accepts_declared_alias_pattern_and_normalized_thumbnail(sel
"https://upload.wikimedia.org/wikipedia/commons/thumb/a/a1/Earth.jpg/320px-Earth.jpg"}]}},
[{"id": "earth", "urls": [
"https://upload.wikimedia.org/wikipedia/commons/thumb/a/a1/Earth.jpg/1280px-Earth.jpg"
+ ], "aliases": [
+ "https://upload.wikimedia.org/wikipedia/commons/thumb/a/a1/Earth.jpg/320px-Earth.jpg"
]}], base_url="https://commons.wikimedia.org/wiki/File:Earth.jpg",
output_contract=contract,
)
@@ -333,14 +335,19 @@ def test_wikimedia_host_equivalence_is_only_for_the_same_thumbnail_asset(self) -
'Moon.jpg/960px-Moon.jpg?utm_campaign=parser')
invalid_original = 'https://thumb.wikimedia.org/wikipedia/commons/4/41/Moon.jpg'
upload_original = 'https://upload.wikimedia.org/wikipedia/commons/4/41/Moon.jpg'
- self.assertEqual(normalize_resource_url(upload_thumb, base),
- normalize_resource_url(migrated_thumb, base))
+ self.assertNotEqual(normalize_resource_url(upload_thumb, base),
+ normalize_resource_url(migrated_thumb, base))
+ declared = [{'id': 'moon', 'urls': [upload_thumb], 'aliases': [migrated_thumb]}]
+ self.assertTrue(evaluate_resources(migrated_thumb, {}, declared,
+ base_url=base)['resource_pass'])
+ self.assertFalse(evaluate_resources(wrong_asset, {}, declared,
+ base_url=base)['resource_pass'])
self.assertNotEqual(normalize_resource_url(upload_thumb, base),
normalize_resource_url(wrong_asset, base))
self.assertNotEqual(normalize_resource_url(invalid_original, base),
normalize_resource_url(upload_original, base))
- def test_image_scope_accepts_bare_declared_extensionless_target(self) -> None:
+ def test_image_scope_rejects_extra_bare_extensionless_url(self) -> None:
target = "https://images.unsplash.com/photo-1493673272479-a20888bcee10"
result = evaluate_resources(
target + "\nhttps://example.test/ordinary-page", {},
@@ -348,8 +355,39 @@ def test_image_scope_accepts_bare_declared_extensionless_target(self) -> None:
base_url="https://unsplash.com/photos/example",
output_contract={"candidate_scope": "image_answer_fields", "unexpected": "reject"},
)
- self.assertTrue(result["resource_pass"])
- self.assertEqual(result["resource_candidate_urls"], [target])
+ self.assertFalse(result["resource_pass"])
+ self.assertEqual(result["resource_candidate_urls"],
+ ["https://example.test/ordinary-page", target])
+ self.assertEqual(result["resource_precision"], 0.5)
+
+ def test_resource_queries_are_not_merged_by_known_host(self):
+ base = 'https://example.test/gallery'
+ for host in ('images.unsplash.com', 'images.pexels.com', 'images-assets.nasa.gov',
+ 'www.nasa.gov', 'cdn.example.test'):
+ with self.subTest(host=host):
+ left, right = f'https://{host}/photo.jpg?crop=left', f'https://{host}/photo.jpg?crop=right'
+ self.assertNotEqual(normalize_resource_url(left, base), normalize_resource_url(right, base))
+ self.assertFalse(evaluate_resources(right, {}, [{'id': 'photo', 'urls': [left]}],
+ base_url=base)['resource_pass'])
+ self.assertTrue(evaluate_resources(right, {}, [{'id': 'photo', 'urls': [left],
+ 'aliases': [right]}], base_url=base)['resource_pass'])
+
+ def test_extensionless_image_candidates_do_not_depend_on_reference(self):
+ right, wrong = 'https://cdn.example/item/RIGHT', 'https://cdn.example/item/WRONG'
+ contract = {'candidate_scope': 'image_answer_fields', 'unexpected': 'reject'}
+ mixed = right + '\n' + wrong
+ candidates = []
+ for target in (right, wrong):
+ result = evaluate_resources(mixed, {}, [{'id': 'image', 'urls': [target]}],
+ base_url='https://example.test/', output_contract=contract)
+ self.assertFalse(result['resource_pass'])
+ self.assertEqual(result['resource_precision'], 0.5)
+ candidates.append(result['resource_candidate_urls'])
+ self.assertEqual(candidates[0], candidates[1])
+ result = evaluate_resources(right, {}, [{'id': 'image', 'urls': [right]}],
+ base_url='https://example.test/', output_contract=contract)
+ self.assertTrue(result['resource_pass'])
+ self.assertEqual(result['resource_precision'], 1.0)
if __name__ == "__main__":
diff --git a/tests/test_providers.py b/tests/test_providers.py
index e2ce37ec9..582d82c7e 100644
--- a/tests/test_providers.py
+++ b/tests/test_providers.py
@@ -8,13 +8,13 @@
from unittest.mock import patch
from harness.web_fetch.metrics import evaluate_resources, task_passed, task_measured
-from harness.web_fetch.models import EvalCase
+from harness.web_fetch.models import EvalCase, ProviderResult
from harness.web_fetch.providers.cloudflare import (
CloudflareContentProvider,
CloudflareMarkdownProvider,
_post_multipart,
)
-from harness.web_fetch.providers.cowagent import CowAgentBuiltinProvider, _sanitize_public_content
+from harness.web_fetch.providers.cowagent import CowAgentBuiltinProvider
from harness.web_fetch.providers.exa import ExaContentsProvider
from harness.web_fetch.providers.firecrawl import FirecrawlProvider
from harness.web_fetch.providers.http import HttpProvider
@@ -1069,7 +1069,7 @@ def test_cloudflare_image_resource_uses_scrape_and_resolves_srcset(
self.assertEqual(selectors, [{"selector": "img"}, {"selector": "picture source"}])
@patch("harness.web_fetch.providers.cloudflare.request")
- def test_cloudflare_video_resource_scrape_filters_ordinary_links(
+ def test_cloudflare_video_resource_preserves_all_declared_links(
self, mock_request
) -> None:
mock_request.return_value = HttpResponse(
@@ -1102,9 +1102,9 @@ def test_cloudflare_video_resource_scrape_filters_ordinary_links(
).fetch(video, 45)
self.assertEqual(result.metadata["videos"], [
- "https://example.com/watch/clip.webm"
+ "https://example.com/about", "https://example.com/watch/clip.webm"
])
- self.assertNotIn("/about", result.content)
+ self.assertEqual(json.loads(result.content), mock_request.return_value.json()['result'])
@patch("harness.web_fetch.providers.cloudflare.request")
def test_cloudflare_video_resource_keeps_packaged_video_downloads(
@@ -1143,11 +1143,11 @@ def test_cloudflare_video_resource_keeps_packaged_video_downloads(
self.assertTrue(result.ok)
self.assertEqual(result.metadata["videos"], [
- "https://example.com/movies/movie.mp4.zip",
+ "https://example.com/", "https://example.com/movies/movie.mp4.zip",
"https://example.com/movies/movie.webm.zip?mirror=1",
+ "https://example.com/movies/notes.zip",
])
- self.assertNotIn("notes.zip", result.content)
- self.assertNotIn("\nhttps://example.com/\n", "\n" + result.content + "\n")
+ self.assertEqual(json.loads(result.content), mock_request.return_value.json()['result'])
@patch("harness.web_fetch.providers.cloudflare.request")
def test_cloudflare_video_link_task_keeps_page_href_without_media_suffix(
@@ -1193,11 +1193,10 @@ def test_cloudflare_video_link_task_keeps_page_href_without_media_suffix(
self.assertTrue(result.ok)
self.assertEqual(result.metadata["videos"], [
+ "https://example.com/about", "https://example.com/short/iframe.html",
"https://example.com/video/BV1234567890",
])
- self.assertIn("https://example.com/video/BV1234567890", result.content)
- self.assertNotIn("https://example.com/about", result.content)
- self.assertNotIn("https://example.com/short/iframe.html", result.content)
+ self.assertEqual(json.loads(result.content), mock_request.return_value.json()['result'])
self.assertEqual(mock_request.call_args.kwargs["json_body"]["elements"],
[{"selector": "a[href]"}])
@@ -1426,15 +1425,18 @@ def test_cowagent_cannot_omit_declared_identity(self) -> None:
self.assertIn("expected_repo_revision", errors)
self.assertIn("required runtime paths", errors)
- def test_cowagent_removes_workspace_path_but_keeps_document_text(self) -> None:
- content = (
- "[Document: guide.pdf | Size: 10KB | Saved to: "
- "/Users/person/.browser-eval/runs/work/tmp/guide.pdf]\n\nBody"
- )
- self.assertEqual(
- _sanitize_public_content(content),
- "[Document: guide.pdf | Size: 10KB]\n\nBody",
- )
+ @patch('harness.web_fetch.providers.subprocess_jsonl.SubprocessJsonlProvider.fetch')
+ def test_cowagent_preserves_native_answer_and_error_before_scoring(self, fetch):
+ content = ('[Document: guide.pdf | Size: 10KB | Saved to: '
+ '/Users/person/work/guide.pdf]\n\nBody')
+ native = ProviderResult(False, content, error_message=content,
+ metadata={'raw_response_sha256': 'native-envelope'})
+ fetch.return_value = native
+ result = CowAgentBuiltinProvider('cowagent', {'repo_path': '.'}).fetch(self.case, 1)
+ self.assertIs(result, native)
+ self.assertEqual(result.content, content)
+ self.assertEqual(result.error_message, content)
+ self.assertEqual(result.metadata, {'raw_response_sha256': 'native-envelope'})
if __name__ == "__main__":
diff --git a/tests/test_report_metrics.py b/tests/test_report_metrics.py
index 3289afa98..a816ce289 100644
--- a/tests/test_report_metrics.py
+++ b/tests/test_report_metrics.py
@@ -24,9 +24,12 @@ def test_cjk_and_markdown(self):
def test_url_echo_is_not_resource_discovery(self):
r=evaluate_resources('https://example.com/a.png',{},[{'urls':['https://example.com/a.png']}],base_url='https://example.com/a.png')
self.assertFalse(r['resource_pass'])
- def test_resized_image_same_asset(self):
- r=evaluate_resources('https://images.pexels.com/photos/123/a.jpeg?w=500',{},[{'urls':['https://images.pexels.com/photos/123/a.jpeg?w=1000']}],base_url='https://example.com')
+ def test_declared_resized_image_alias(self):
+ target='https://images.pexels.com/photos/123/a.jpeg?w=1000'
+ resized='https://images.pexels.com/photos/123/a.jpeg?w=500'
+ r=evaluate_resources(resized,{},[{'urls':[target],'aliases':[resized]}],base_url='https://example.com')
self.assertTrue(r['resource_pass'])
+ self.assertFalse(evaluate_resources(resized,{},[{'urls':[target]}],base_url='https://example.com')['resource_pass'])
def test_generic_query_not_dropped(self):
r=evaluate_resources('https://example.com/file?id=2',{},[{'urls':['https://example.com/file?id=1']}],base_url='https://example.com')
self.assertFalse(r['resource_pass'])
diff --git a/tests/test_subprocess_provider.py b/tests/test_subprocess_provider.py
index af9ec302f..46d458b18 100644
--- a/tests/test_subprocess_provider.py
+++ b/tests/test_subprocess_provider.py
@@ -124,6 +124,68 @@ def test_worker_receives_shared_limits_and_retains_full_response_envelope(self):
persist_result_artifacts(target, Path('artifacts/worker/control.txt'), result)
self.assertEqual(json.loads((target/result.metadata['raw_response_path']).read_bytes()),payload)
+ def test_persistence_rejects_answer_edit_or_candidate_filtering(self):
+ import hashlib
+ from harness.web_fetch.models import ProviderResult
+ from harness.web_fetch.runner import persist_result_artifacts
+ from harness.web_fetch.response_input import project_native_answer
+ cases = [
+ ({'content': 'Body | Saved to: /Users/person/work/page.txt'},
+ {'schema_version': 1, 'content_pointer': ['content'], 'content_format': 'text'}),
+ ({'result': [{'selector': 'a[href]', 'results': [{'attributes': [
+ {'name': 'href', 'value': '/opaque-resource'},
+ {'name': 'href', 'value': '/about'},
+ {'name': 'title', 'value': 'Native label'}]}]}]},
+ {'schema_version': 1, 'content_pointer': ['result'], 'content_format': 'json',
+ 'answer_fields': {'videos': {'pointer': ['result'],
+ 'format': 'html_attribute_urls', 'base_url': 'https://example.test/'}}}),
+ ]
+ for payload, declaration in cases:
+ content, fields = project_native_answer(payload, declaration)
+ raw = json.dumps(payload).encode()
+ for defect in (None, 'edited_text', 'filtered_field', 'missing_raw'):
+ if defect == 'filtered_field' and not fields:
+ continue
+ with self.subTest(format=declaration['content_format'], defect=defect):
+ metadata = {**fields, 'native_answer_projection': declaration,
+ '_raw_response_b64': base64.b64encode(raw).decode(),
+ 'raw_response_sha256': hashlib.sha256(raw).hexdigest(),
+ 'raw_response_bytes': len(raw)}
+ answer = content
+ if defect == 'edited_text': answer = content.replace('/about', '') + 'changed'
+ if defect == 'filtered_field': metadata['videos'] = metadata['videos'][:1]
+ if defect == 'missing_raw': metadata.pop('_raw_response_b64')
+ result = ProviderResult(True, answer, metadata=metadata)
+ with tempfile.TemporaryDirectory() as directory:
+ target = Path(directory)/'artifacts/control/result.txt'
+ if defect:
+ with self.assertRaisesRegex(ValueError, 'native'):
+ persist_result_artifacts(Path(directory),
+ Path('artifacts/control/result.txt'), result)
+ self.assertFalse(target.exists())
+ else:
+ persist_result_artifacts(Path(directory),
+ Path('artifacts/control/result.txt'), result)
+ self.assertEqual(target.read_text(), content)
+ if declaration['content_format'] == 'json':
+ self.assertEqual(json.loads(content), payload['result'])
+
+ def test_deadline_discards_answer_without_discarding_native_evidence(self):
+ import hashlib
+ from harness.web_fetch.models import ProviderResult
+ from harness.web_fetch.runner import persist_result_artifacts
+ raw = json.dumps({'content': 'late native answer'}).encode()
+ answer = ProviderResult(False, '', error_type='timeout', metadata={
+ 'native_answer_projection': {'schema_version': 1,
+ 'content_pointer': ['content'], 'content_format': 'text'},
+ '_raw_response_b64': base64.b64encode(raw).decode(),
+ 'raw_response_sha256': hashlib.sha256(raw).hexdigest(),
+ 'raw_response_bytes': len(raw)})
+ with tempfile.TemporaryDirectory() as directory:
+ persist_result_artifacts(Path(directory), Path('artifacts/worker/late.txt'), answer)
+ self.assertEqual((Path(directory)/'artifacts/worker/late.txt').read_bytes(), b'')
+ self.assertEqual((Path(directory)/answer.metadata['raw_response_path']).read_bytes(), raw)
+
def test_native_langchain_document_is_retained_without_changing_answer(self):
import threading
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
From ec71b5252eaf164754d0f852d3a62ca4cbb1196e Mon Sep 17 00:00:00 2001
From: Maisiqi <324751762+Maisiqi@users.noreply.github.com>
Date: Fri, 16 Oct 2026 00:08:10 +0800
Subject: [PATCH 26/50] Restore scored answers from bound native envelopes
without new calls
---
evaluation/scoring-regressions.json | 106 ++++++++++++++++++++++++++++
harness/web_fetch/local_revision.py | 84 +++++++++++++++++++++-
harness/web_fetch/metrics.py | 25 ++++---
tests/test_local_revision.py | 84 ++++++++++++++++++++++
4 files changed, 287 insertions(+), 12 deletions(-)
diff --git a/evaluation/scoring-regressions.json b/evaluation/scoring-regressions.json
index 4a05dbb98..c9b81ae89 100644
--- a/evaluation/scoring-regressions.json
+++ b/evaluation/scoring-regressions.json
@@ -31732,6 +31732,112 @@
],
"resource_pass": true
}
+ },
+ {
+ "id": "native-json-resource-untyped-url-echo-rejected",
+ "category": "acceptance-contract",
+ "root_cause": "native_json_answer_field_scope",
+ "failure_mode": "false-positive",
+ "case": {
+ "url": "https://example.test/gallery",
+ "task_kind": "resource",
+ "content_score_applicable": false,
+ "expected_resources": [
+ {
+ "id": "target",
+ "urls": [
+ "https://cdn.example.test/target.jpg"
+ ]
+ }
+ ],
+ "resource_output_contract": {
+ "candidate_scope": "image_answer_fields",
+ "unexpected": "reject"
+ }
+ },
+ "artifact": "{\"echo\": \"https://cdn.example.test/target.jpg\"}",
+ "metadata": {
+ "images": [
+ "https://cdn.example.test/other.jpg"
+ ],
+ "native_answer_projection": {
+ "schema_version": 1,
+ "content_pointer": [
+ "result"
+ ],
+ "content_format": "json",
+ "answer_fields": {
+ "images": {
+ "pointer": [
+ "result",
+ "images"
+ ],
+ "format": "identity"
+ }
+ }
+ }
+ },
+ "ok": true,
+ "expected_task_pass": false,
+ "expected_metrics": {
+ "resource_candidate_urls": [
+ "https://cdn.example.test/other.jpg"
+ ],
+ "resource_precision": 0.0
+ }
+ },
+ {
+ "id": "native-json-resource-declared-image-field-preserved",
+ "category": "acceptance-contract",
+ "root_cause": "native_json_answer_field_scope",
+ "failure_mode": "false-negative",
+ "case": {
+ "url": "https://example.test/gallery",
+ "task_kind": "resource",
+ "content_score_applicable": false,
+ "expected_resources": [
+ {
+ "id": "target",
+ "urls": [
+ "https://cdn.example.test/target.jpg"
+ ]
+ }
+ ],
+ "resource_output_contract": {
+ "candidate_scope": "image_answer_fields",
+ "unexpected": "reject"
+ }
+ },
+ "artifact": "{\"echo\": \"https://cdn.example.test/other.jpg\"}",
+ "metadata": {
+ "images": [
+ "https://cdn.example.test/target.jpg"
+ ],
+ "native_answer_projection": {
+ "schema_version": 1,
+ "content_pointer": [
+ "result"
+ ],
+ "content_format": "json",
+ "answer_fields": {
+ "images": {
+ "pointer": [
+ "result",
+ "images"
+ ],
+ "format": "identity"
+ }
+ }
+ }
+ },
+ "ok": true,
+ "expected_task_pass": true,
+ "expected_metrics": {
+ "resource_candidate_urls": [
+ "https://cdn.example.test/target.jpg"
+ ],
+ "resource_precision": 1.0
+ }
}
],
"superseded_controls": {
diff --git a/harness/web_fetch/local_revision.py b/harness/web_fetch/local_revision.py
index 8024a8027..224c82c2d 100644
--- a/harness/web_fetch/local_revision.py
+++ b/harness/web_fetch/local_revision.py
@@ -1,12 +1,14 @@
"""Validate task replacements and offline revisions of a bound local snapshot."""
import hashlib
import json
+from pathlib import Path
from harness.record_io import load_jsonl
from .cohort_exclusions import require_cohort_exclusions
from .safety import contained_path
from .scoring_regressions import require_scoring_regressions
+from .scoring_revision import ALLOWED_FIELDS
def read_bound(source, binding):
@@ -92,6 +94,41 @@ def require_local_revision(source, binding, dataset, records):
return validate_revision_chain(source, binding, dataset, records)[0]
+def _require_native_answer_correction(source, original, corrected, proof):
+ """Verify a derived answer against the same immutable native response."""
+ from .response_input import project_native_answer
+ if proof.get('attempt_id') != original['attempt_id']:
+ raise ValueError('Native answer correction lost its attempt identity')
+ native = read_bound(source, proof['native_response'])
+ metadata = original.get('provider_metadata', {})
+ if (proof['native_response']['sha256'] != metadata.get('raw_response_sha256')
+ or native.stat().st_size != metadata.get('raw_response_bytes')):
+ raise ValueError('Native answer correction changed its original response')
+ before = proof['original_artifact']
+ after = proof['corrected_artifact']
+ if (before['path'] != original['artifact_path']
+ or before['sha256'] != original['metrics']['content_sha256']
+ or after['path'] != corrected['artifact_path']
+ or after['sha256'] != corrected['metrics']['content_sha256']
+ or before['path'] == after['path']):
+ raise ValueError('Native answer correction must preserve its old artifact')
+ read_bound(source, before)
+ text, fields = project_native_answer(json.loads(native.read_bytes()), proof['projection'])
+ if read_bound(source, after).read_bytes() != text.encode('utf-8', errors='replace'):
+ raise ValueError('Native answer correction differs from declared original fields')
+ for field in proof['projection'].get('answer_fields', {}).values():
+ if field.get('format') == 'html_attribute_urls' and field.get('base_url') != original['url']:
+ raise ValueError('Native URL projection must use the original request URL')
+ expected_metadata = {**metadata, **fields, 'native_answer_projection': proof['projection']}
+ if corrected.get('provider_metadata') != expected_metadata:
+ raise ValueError('Native answer correction changed unrelated response metadata')
+ derived = {'metrics', 'scoring', 'diagnosis', 'failure_category',
+ 'artifact_path', 'provider_metadata'}
+ if ({k: v for k, v in original.items() if k not in derived}
+ != {k: v for k, v in corrected.items() if k not in derived}):
+ raise ValueError('Native answer correction changed physical call facts')
+
+
def _require_revision_step(source, receipt, dataset, records, outputs):
if receipt.get('kind') != 'local_offline_score_revision' or receipt.get('provider_calls_added') != 0:
raise ValueError('Local revision must preserve original provider calls')
@@ -122,7 +159,7 @@ def _require_revision_step(source, receipt, dataset, records, outputs):
for identifier, old in old_cases.items():
new = new_cases[identifier]
fields = {key for key in old.keys() | new.keys() if old.get(key) != new.get(key)}
- if fields - {'task_prompt', 'acceptance_contract', 'body_requirements', 'required_patterns', 'required_facts', 'forbidden_any', 'diagnostic_any', 'acceptance_policy_version'}:
+ if fields - (ALLOWED_FIELDS | {'diagnostic_any', 'acceptance_policy_version'}):
raise ValueError('Local score revision changed source or acquisition inputs')
if fields:
changed_cases.add(identifier)
@@ -137,10 +174,17 @@ def _require_revision_step(source, receipt, dataset, records, outputs):
scored = set(receipt.get('rescored_attempt_ids', []))
if not scored <= set(new_rows):
raise ValueError('Local revision rescoring scope differs')
+ native_proofs = receipt.get('native_answer_corrections', [])
+ corrected_answers = {item['attempt_id']: item for item in native_proofs}
+ if (len(corrected_answers) != len(native_proofs)
+ or not set(corrected_answers) <= scored):
+ raise ValueError('Native answer correction scope differs from rescoring scope')
expected_scoring = {}
for identifier, old in old_rows.items():
new = new_rows[identifier]
- if ({k: v for k, v in old.items() if k not in {'metrics', 'scoring', 'diagnosis', 'failure_category'}}
+ if identifier in corrected_answers:
+ _require_native_answer_correction(source, old, new, corrected_answers[identifier])
+ elif ({k: v for k, v in old.items() if k not in {'metrics', 'scoring', 'diagnosis', 'failure_category'}}
!= {k: v for k, v in new.items() if k not in {'metrics', 'scoring', 'diagnosis', 'failure_category'}}
or old['metrics']['content_sha256'] != new['metrics']['content_sha256']):
raise ValueError('Local revision changed measured call facts: ' + identifier)
@@ -224,6 +268,12 @@ def revise_local_snapshot(source, configuration):
history = source / 'local-evidence/offline-revisions' / revision_key
if history.exists():
raise ValueError('Local revision already exists; preserve the prior receipt')
+ native_inputs = config.get('native_answer_corrections', [])
+ native_requests = {item['attempt_id']: item for item in native_inputs}
+ if (len(native_requests) != len(native_inputs)
+ or not set(native_requests) <= {row['attempt_id'] for row in records}):
+ raise ValueError('Native answer correction has an invalid input scope')
+ additional.update(native_requests)
indexed = {case['id']: case for case in dataset['cases']}
input_verdicts = None
if config.get('input_verdicts'):
@@ -234,6 +284,7 @@ def revise_local_snapshot(source, configuration):
or any(type(value) is not bool for value in input_verdicts.values())):
raise ValueError('Frozen input verdicts do not match the complete input results')
selected = []
+ native_proofs = []
before_verdicts = {}
for row in records:
if row['case_id'] not in replacements and row['attempt_id'] not in additional:
@@ -245,9 +296,37 @@ def revise_local_snapshot(source, configuration):
before = (input_verdicts[row['attempt_id']] if input_verdicts is not None
else task_passed(row['ok'], row['metrics']))
before_verdicts[row['attempt_id']] = before
+ if row['attempt_id'] in native_requests:
+ from .response_input import project_native_answer
+ request = native_requests[row['attempt_id']]
+ native_path = read_bound(source, request['native_response'])
+ metadata = row.get('provider_metadata', {})
+ if (request['native_response']['sha256'] != metadata.get('raw_response_sha256')
+ or native_path.stat().st_size != metadata.get('raw_response_bytes')):
+ raise ValueError('Native correction source differs from its original response')
+ answer, fields = project_native_answer(json.loads(native_path.read_bytes()), request['projection'])
+ raw = answer.encode('utf-8', errors='replace')
+ original_artifact = {'path': row['artifact_path'], 'sha256': row['metrics']['content_sha256']}
+ digest = hashlib.sha256(raw).hexdigest()
+ old_path = Path(row['artifact_path'])
+ new_relative = old_path.with_name(old_path.stem + '-native-' + digest[:12] + old_path.suffix)
+ corrected_path = contained_path(source, new_relative, reject_symlinks=True)
+ if corrected_path.exists() and corrected_path.read_bytes() != raw:
+ raise ValueError('Native correction would overwrite a different artifact')
+ corrected_path.parent.mkdir(parents=True, exist_ok=True)
+ corrected_path.write_bytes(raw)
+ row['artifact_path'] = new_relative.as_posix()
+ row['provider_metadata'] = {**metadata, **fields, 'native_answer_projection': request['projection']}
+ native_proofs.append({'attempt_id': row['attempt_id'],
+ 'native_response': request['native_response'], 'projection': request['projection'],
+ 'original_artifact': original_artifact,
+ 'corrected_artifact': {'path': row['artifact_path'], 'sha256': digest}})
row['metrics'] = score_attempt(case, raw, ok=row['ok'], status_code=row.get('status_code'),
title=row.get('title'), final_url=row.get('final_url'), metadata=row.get('provider_metadata'),
previous_metrics=row['metrics'], list_reference=load_list_reference(case, source))
+ # Diagnoses describe the former score and must be regenerated from this answer.
+ row.pop('diagnosis', None)
+ row.pop('failure_category', None)
row['scoring'] = {**row.get('scoring', {}), 'mode': 'offline_rescore',
'scored_at': datetime.now(timezone.utc).isoformat(),
'rule_revision': bool(config.get('rule_revision')),
@@ -276,6 +355,7 @@ def binding(path):
'changed_case_ids': sorted(case['id'] for case in dataset['cases']
if case != next(c for c in original_dataset['cases'] if c['id'] == case['id'])),
'rescored_attempt_ids': selected, 'rule_revision': bool(config.get('rule_revision')),
+ **({'native_answer_corrections': native_proofs} if native_proofs else {}),
'input_verdicts': config.get('input_verdicts'),
**({'predecessor_revision': config['predecessor_revision']} if config.get('predecessor_revision') else {}),
'superseded_controls': config.get('superseded_controls', []),
diff --git a/harness/web_fetch/metrics.py b/harness/web_fetch/metrics.py
index 3c21c5999..e7710b8da 100644
--- a/harness/web_fetch/metrics.py
+++ b/harness/web_fetch/metrics.py
@@ -210,6 +210,9 @@ def evaluate_resources(
raise ValueError(f"Unsupported resource unexpected policy: {contract.get('unexpected')!r}")
candidates: list[tuple[str, str]] = []
+ projection = metadata.get('native_answer_projection') or {}
+ typed_json_answer = (projection.get('content_format') == 'json'
+ and bool(projection.get('answer_fields')))
if eligible and candidate_scope == "image_answer_fields":
if "images" in metadata:
candidates.extend(_metadata_image_candidates(
@@ -219,19 +222,21 @@ def evaluate_resources(
for key in ("media", "extras"):
if key in metadata:
candidates.extend(_metadata_image_candidates(metadata[key], path=f"metadata.{key}"))
- candidates.extend(_image_content_candidates(content))
+ if not typed_json_answer:
+ candidates.extend(_image_content_candidates(content))
elif eligible:
answer_fields = {key: metadata[key] for key in ('links','images','videos','media','extras') if key in metadata}
candidates.extend(_metadata_resource_candidates(answer_fields))
- from .reference_quality import markdown_link_destinations
- candidates.extend((url, "content.bare_url") for url in
- re.findall(r"https?://[^\s<>'\")\]]+", content))
- candidates.extend((match[1], "content.html_url") for match in
- re.findall(r"(?:src|href)\s*=\s*([\"'])(.*?)\1", content, re.I))
- candidates.extend((url, "content.markdown_link")
- for url in markdown_link_destinations(content))
- candidates.extend((url, "content.visible_file")
- for url in _visible_relative_resource_urls(content))
+ if not typed_json_answer:
+ from .reference_quality import markdown_link_destinations
+ candidates.extend((url, "content.bare_url") for url in
+ re.findall(r"https?://[^\s<>'\")\]]+", content))
+ candidates.extend((match[1], "content.html_url") for match in
+ re.findall(r"(?:src|href)\s*=\s*([\"'])(.*?)\1", content, re.I))
+ candidates.extend((url, "content.markdown_link")
+ for url in markdown_link_destinations(content))
+ candidates.extend((url, "content.visible_file")
+ for url in _visible_relative_resource_urls(content))
normalized_candidates, candidate_evidence = _normalized_candidates(candidates, base_url=base_url)
results = []
allowed_urls: set[str] = set()
diff --git a/tests/test_local_revision.py b/tests/test_local_revision.py
index 1a3ed0fac..d30bb7a68 100644
--- a/tests/test_local_revision.py
+++ b/tests/test_local_revision.py
@@ -181,3 +181,87 @@ def test_hash_connected_intermediate_revision_must_pass_full_validation(tmp_path
latest_binding['sha256'] = hashlib.sha256(latest_path.read_bytes()).hexdigest()
with pytest.raises(ValueError, match=message):
validate_revision_chain(tmp_path, latest_binding)[1:]
+
+
+def _native_answer_revision_fixture(root):
+ raw = b'Body fact'
+ (root/'answer.txt').write_bytes(raw)
+ native_content = 'Body fact | Saved to: /Users/person/private/page.txt'
+ native = (json.dumps({'content': native_content})+'\n').encode()
+ native_path = root/'private_artifacts/native.response.bin'
+ native_path.parent.mkdir()
+ native_path.write_bytes(native)
+ case = {'id': 'a', 'url': 'https://example.test/',
+ 'required_facts': [{'id': 'body', 'alternatives': ['Body fact']}]}
+ row = {'attempt_id': 'p__a', 'provider': 'p', 'case_id': 'a', 'ok': True,
+ 'url': case['url'], 'artifact_path': 'answer.txt', 'elapsed_ms': 7,
+ 'diagnosis': {'category': 'obsolete diagnosis', 'evidence': 'old answer'},
+ 'failure_category': 'obsolete failure',
+ 'provider_metadata': {'raw_response_path': 'private_artifacts/native.response.bin',
+ 'raw_response_sha256': hashlib.sha256(native).hexdigest(),
+ 'raw_response_bytes': len(native)},
+ 'metrics': score_attempt(case, raw, ok=True)}
+ (root/'dataset.json').write_text(json.dumps({'cases': [case], 'comparison': {}}))
+ (root/'results.jsonl').write_text(json.dumps(row)+'\n')
+ config = {'cases': [], 'authority': {'description': 'Restore declared native fields from the same call; preserve the former derived answer and measured time.'},
+ 'regression_ids': ['native-json-resource-declared-image-field-preserved',
+ 'native-json-resource-untyped-url-echo-rejected'],
+ 'native_answer_corrections': [{'attempt_id': row['attempt_id'],
+ 'native_response': {'path': row['provider_metadata']['raw_response_path'],
+ 'sha256': row['provider_metadata']['raw_response_sha256']},
+ 'projection': {'schema_version': 1, 'content_pointer': ['content'],
+ 'content_format': 'text'}}]}
+ config_path = root/'correction.json'
+ config_path.write_text(json.dumps(config))
+ receipt = revise_local_snapshot(root, config_path)
+ receipt_path = root/receipt['input_dataset']['path']
+ receipt_path = receipt_path.parent/'receipt.json'
+ binding = {'path': str(receipt_path.relative_to(root)),
+ 'sha256': hashlib.sha256(receipt_path.read_bytes()).hexdigest()}
+ return row, receipt, receipt_path, binding, native_content
+
+
+def test_offline_native_correction_preserves_call_and_both_answers(tmp_path):
+ original, receipt, _, binding, native_content = _native_answer_revision_fixture(tmp_path)
+ current = json.loads((tmp_path/'results.jsonl').read_text())
+ assert receipt['provider_calls_added'] == 0
+ assert receipt['changed_case_ids'] == []
+ assert receipt['task_verdict_changes'] == []
+ assert current['elapsed_ms'] == original['elapsed_ms']
+ assert 'diagnosis' not in current
+ assert 'failure_category' not in current
+ assert (tmp_path/'answer.txt').read_bytes() == b'Body fact'
+ assert current['artifact_path'] != original['artifact_path']
+ assert (tmp_path/current['artifact_path']).read_text() == native_content
+ assert len(receipt['native_answer_corrections']) == 1
+ require_local_revision(tmp_path, binding, json.loads((tmp_path/'dataset.json').read_text()), [current])
+
+
+@pytest.mark.parametrize('defect,message', [
+ ('clock', 'physical call facts'), ('metadata', 'unrelated response metadata'),
+ ('answer', 'declared original fields'), ('source', 'original response'),
+ ('missing_proof', 'measured call facts'),
+])
+def test_native_correction_rejects_self_consistent_but_false_projection(tmp_path, defect, message):
+ _, receipt, path, binding, _ = _native_answer_revision_fixture(tmp_path)
+ current = json.loads((tmp_path/'results.jsonl').read_text())
+ proof = receipt['native_answer_corrections'][0]
+ if defect == 'clock': current['elapsed_ms'] += 1
+ elif defect == 'metadata': current['provider_metadata']['injected'] = 'new answer data'
+ elif defect == 'answer':
+ artifact = tmp_path/current['artifact_path']
+ artifact.write_text('Different answer not supplied by the call')
+ digest = hashlib.sha256(artifact.read_bytes()).hexdigest()
+ current['metrics']['content_sha256'] = digest
+ proof['corrected_artifact']['sha256'] = digest
+ elif defect == 'source':
+ source = tmp_path/proof['native_response']['path']
+ source.write_text(json.dumps({'content': 'Different native response'}))
+ proof['native_response']['sha256'] = hashlib.sha256(source.read_bytes()).hexdigest()
+ else: receipt.pop('native_answer_corrections')
+ (tmp_path/'results.jsonl').write_text(json.dumps(current)+'\n')
+ receipt['output_results_sha256'] = hashlib.sha256((tmp_path/'results.jsonl').read_bytes()).hexdigest()
+ path.write_text(json.dumps(receipt))
+ binding['sha256'] = hashlib.sha256(path.read_bytes()).hexdigest()
+ with pytest.raises(ValueError, match=message):
+ require_local_revision(tmp_path, binding, json.loads((tmp_path/'dataset.json').read_text()), [current])
From 10256084727fd5b499812976c96c9942f95e5686 Mon Sep 17 00:00:00 2001
From: Maisiqi <324751762+Maisiqi@users.noreply.github.com>
Date: Fri, 16 Oct 2026 00:18:28 +0800
Subject: [PATCH 27/50] Bind explicit image equivalents without rewriting
source observations
---
evaluation/scoring-regressions.json | 362 +++++++++++++++++++++++
harness/web_fetch/reference_lifecycle.py | 56 +++-
tests/test_reference_lifecycle.py | 53 ++++
3 files changed, 469 insertions(+), 2 deletions(-)
diff --git a/evaluation/scoring-regressions.json b/evaluation/scoring-regressions.json
index c9b81ae89..2fb664fc6 100644
--- a/evaluation/scoring-regressions.json
+++ b/evaluation/scoring-regressions.json
@@ -31838,6 +31838,368 @@
],
"resource_precision": 1.0
}
+ },
+ {
+ "id": "image-04-declared-display-equivalence-accepted",
+ "category": "acceptance-contract",
+ "root_cause": "task_declared_image_display_equivalence",
+ "failure_mode": "false-negative",
+ "case": {
+ "url": "https://en.wikipedia.org/wiki/Apollo_11",
+ "track": "image",
+ "task_kind": "resource",
+ "content_score_applicable": false,
+ "expected_resources": [
+ {
+ "id": "image-1",
+ "kind": "image-1",
+ "urls": [
+ "https://upload.wikimedia.org/wikipedia/commons/thumb/4/41/A_Man_on_the_Moon%2C_AS11-40-5903_%28cropped%29.jpg/330px-A_Man_on_the_Moon%2C_AS11-40-5903_%28cropped%29.jpg?utm_source=en.wikipedia.org&utm_campaign=parser&utm_content=thumbnail"
+ ],
+ "url_patterns": [
+ "^https://(?:upload|thumb)\\.wikimedia\\.org/wikipedia/commons/(?:thumb/)?4/41/A_Man_on_the_Moon,_AS11\\-40\\-5903_\\(cropped\\)\\.jpg(?:/[1-9][0-9]*px-A_Man_on_the_Moon,_AS11\\-40\\-5903_\\(cropped\\)\\.jpg)?(?:\\?(?:utm_source=(?:en\\.wikipedia\\.org|commons\\.wikimedia\\.org)|utm_campaign=(?:parser|index)|utm_content=thumbnail|_=[0-9]+)(?:&(?:utm_source=(?:en\\.wikipedia\\.org|commons\\.wikimedia\\.org)|utm_campaign=(?:parser|index)|utm_content=thumbnail|_=[0-9]+))*)?$"
+ ]
+ },
+ {
+ "id": "image-2",
+ "kind": "image-2",
+ "urls": [
+ "https://upload.wikimedia.org/wikipedia/commons/thumb/2/27/Apollo_11_insignia.png/250px-Apollo_11_insignia.png?utm_source=en.wikipedia.org&utm_campaign=parser&utm_content=thumbnail"
+ ],
+ "url_patterns": [
+ "^https://(?:upload|thumb)\\.wikimedia\\.org/wikipedia/commons/(?:thumb/)?2/27/Apollo_11_insignia\\.png(?:/[1-9][0-9]*px-Apollo_11_insignia\\.png)?(?:\\?(?:utm_source=(?:en\\.wikipedia\\.org|commons\\.wikimedia\\.org)|utm_campaign=(?:parser|index)|utm_content=thumbnail|_=[0-9]+)(?:&(?:utm_source=(?:en\\.wikipedia\\.org|commons\\.wikimedia\\.org)|utm_campaign=(?:parser|index)|utm_content=thumbnail|_=[0-9]+))*)?$"
+ ]
+ }
+ ],
+ "resource_output_contract": {
+ "candidate_scope": "image_answer_fields",
+ "unexpected": "allow"
+ }
+ },
+ "artifact": "https://upload.wikimedia.org/wikipedia/commons/thumb/4/41/A_Man_on_the_Moon%2C_AS11-40-5903_%28cropped%29.jpg/640px-A_Man_on_the_Moon%2C_AS11-40-5903_%28cropped%29.jpg\nhttps://upload.wikimedia.org/wikipedia/commons/thumb/2/27/Apollo_11_insignia.png/640px-Apollo_11_insignia.png",
+ "ok": true,
+ "expected_task_pass": true,
+ "expected_metrics": {
+ "resource_pass": true
+ }
+ },
+ {
+ "id": "image-04-declared-display-equivalence-unlisted-query-rejected",
+ "category": "acceptance-contract",
+ "root_cause": "task_declared_image_display_equivalence",
+ "failure_mode": "false-positive",
+ "case": {
+ "url": "https://en.wikipedia.org/wiki/Apollo_11",
+ "track": "image",
+ "task_kind": "resource",
+ "content_score_applicable": false,
+ "expected_resources": [
+ {
+ "id": "image-1",
+ "kind": "image-1",
+ "urls": [
+ "https://upload.wikimedia.org/wikipedia/commons/thumb/4/41/A_Man_on_the_Moon%2C_AS11-40-5903_%28cropped%29.jpg/330px-A_Man_on_the_Moon%2C_AS11-40-5903_%28cropped%29.jpg?utm_source=en.wikipedia.org&utm_campaign=parser&utm_content=thumbnail"
+ ],
+ "url_patterns": [
+ "^https://(?:upload|thumb)\\.wikimedia\\.org/wikipedia/commons/(?:thumb/)?4/41/A_Man_on_the_Moon,_AS11\\-40\\-5903_\\(cropped\\)\\.jpg(?:/[1-9][0-9]*px-A_Man_on_the_Moon,_AS11\\-40\\-5903_\\(cropped\\)\\.jpg)?(?:\\?(?:utm_source=(?:en\\.wikipedia\\.org|commons\\.wikimedia\\.org)|utm_campaign=(?:parser|index)|utm_content=thumbnail|_=[0-9]+)(?:&(?:utm_source=(?:en\\.wikipedia\\.org|commons\\.wikimedia\\.org)|utm_campaign=(?:parser|index)|utm_content=thumbnail|_=[0-9]+))*)?$"
+ ]
+ },
+ {
+ "id": "image-2",
+ "kind": "image-2",
+ "urls": [
+ "https://upload.wikimedia.org/wikipedia/commons/thumb/2/27/Apollo_11_insignia.png/250px-Apollo_11_insignia.png?utm_source=en.wikipedia.org&utm_campaign=parser&utm_content=thumbnail"
+ ],
+ "url_patterns": [
+ "^https://(?:upload|thumb)\\.wikimedia\\.org/wikipedia/commons/(?:thumb/)?2/27/Apollo_11_insignia\\.png(?:/[1-9][0-9]*px-Apollo_11_insignia\\.png)?(?:\\?(?:utm_source=(?:en\\.wikipedia\\.org|commons\\.wikimedia\\.org)|utm_campaign=(?:parser|index)|utm_content=thumbnail|_=[0-9]+)(?:&(?:utm_source=(?:en\\.wikipedia\\.org|commons\\.wikimedia\\.org)|utm_campaign=(?:parser|index)|utm_content=thumbnail|_=[0-9]+))*)?$"
+ ]
+ }
+ ],
+ "resource_output_contract": {
+ "candidate_scope": "image_answer_fields",
+ "unexpected": "allow"
+ }
+ },
+ "artifact": "https://upload.wikimedia.org/wikipedia/commons/thumb/4/41/A_Man_on_the_Moon%2C_AS11-40-5903_%28cropped%29.jpg/640px-A_Man_on_the_Moon%2C_AS11-40-5903_%28cropped%29.jpg?crop=unapproved\nhttps://upload.wikimedia.org/wikipedia/commons/thumb/2/27/Apollo_11_insignia.png/640px-Apollo_11_insignia.png",
+ "ok": true,
+ "expected_task_pass": false,
+ "expected_metrics": {
+ "resource_pass": false
+ }
+ },
+ {
+ "id": "image-nasa-article-declared-display-equivalence-accepted",
+ "category": "acceptance-contract",
+ "root_cause": "task_declared_image_display_equivalence",
+ "failure_mode": "false-negative",
+ "case": {
+ "url": "https://www.nasa.gov/image-article/historic-engines-take-their-place-on-artemis-iii/",
+ "track": "image",
+ "task_kind": "resource",
+ "content_score_applicable": false,
+ "expected_resources": [
+ {
+ "id": "main-image",
+ "kind": "image",
+ "urls": [
+ "https://images-assets.nasa.gov/image/KSC-20260825-PH-RCR01_0045/KSC-20260825-PH-RCR01_0045~large.jpg?w=1920&h=1280&fit=clip&crop=faces%2Cfocalpoint"
+ ],
+ "url_patterns": [
+ "^https://images\\-assets\\.nasa\\.gov/image/KSC\\-20260825\\-PH\\-RCR01_0045/KSC\\-20260825\\-PH\\-RCR01_0045\\~large\\.jpg(?:\\?(?:[wh]=[1-9][0-9]*|fit=clip|crop=faces(?:%2[Cc]|,)focalpoint)(?:&(?:[wh]=[1-9][0-9]*|fit=clip|crop=faces(?:%2[Cc]|,)focalpoint))*)?$"
+ ]
+ }
+ ],
+ "resource_output_contract": {
+ "candidate_scope": "image_answer_fields",
+ "unexpected": "allow"
+ }
+ },
+ "artifact": "https://images-assets.nasa.gov/image/KSC-20260825-PH-RCR01_0045/KSC-20260825-PH-RCR01_0045~large.jpg?w=640&h=480&fit=clip&crop=faces%2Cfocalpoint",
+ "ok": true,
+ "expected_task_pass": true,
+ "expected_metrics": {
+ "resource_pass": true
+ }
+ },
+ {
+ "id": "image-nasa-article-declared-display-equivalence-unlisted-query-rejected",
+ "category": "acceptance-contract",
+ "root_cause": "task_declared_image_display_equivalence",
+ "failure_mode": "false-positive",
+ "case": {
+ "url": "https://www.nasa.gov/image-article/historic-engines-take-their-place-on-artemis-iii/",
+ "track": "image",
+ "task_kind": "resource",
+ "content_score_applicable": false,
+ "expected_resources": [
+ {
+ "id": "main-image",
+ "kind": "image",
+ "urls": [
+ "https://images-assets.nasa.gov/image/KSC-20260825-PH-RCR01_0045/KSC-20260825-PH-RCR01_0045~large.jpg?w=1920&h=1280&fit=clip&crop=faces%2Cfocalpoint"
+ ],
+ "url_patterns": [
+ "^https://images\\-assets\\.nasa\\.gov/image/KSC\\-20260825\\-PH\\-RCR01_0045/KSC\\-20260825\\-PH\\-RCR01_0045\\~large\\.jpg(?:\\?(?:[wh]=[1-9][0-9]*|fit=clip|crop=faces(?:%2[Cc]|,)focalpoint)(?:&(?:[wh]=[1-9][0-9]*|fit=clip|crop=faces(?:%2[Cc]|,)focalpoint))*)?$"
+ ]
+ }
+ ],
+ "resource_output_contract": {
+ "candidate_scope": "image_answer_fields",
+ "unexpected": "allow"
+ }
+ },
+ "artifact": "https://images-assets.nasa.gov/image/KSC-20260825-PH-RCR01_0045/KSC-20260825-PH-RCR01_0045~large.jpg?w=640&h=480&fit=clip&crop=faces%2Cfocalpoint&crop=unapproved",
+ "ok": true,
+ "expected_task_pass": false,
+ "expected_metrics": {
+ "resource_pass": false
+ }
+ },
+ {
+ "id": "image-unsplash-detail-declared-display-equivalence-accepted",
+ "category": "acceptance-contract",
+ "root_cause": "task_declared_image_display_equivalence",
+ "failure_mode": "false-negative",
+ "case": {
+ "url": "https://unsplash.com/photos/green-leaf-plants-mSK5nNsAsLY",
+ "track": "image",
+ "task_kind": "resource",
+ "content_score_applicable": false,
+ "expected_resources": [
+ {
+ "id": "main-image",
+ "kind": "image",
+ "urls": [
+ "https://images.unsplash.com/photo-1493673272479-a20888bcee10",
+ "https://unsplash.com/photos/mSK5nNsAsLY/download"
+ ],
+ "url_patterns": [
+ "^https://images\\.unsplash\\.com/photo\\-1493673272479\\-a20888bcee10(?:\\?(?:[wh]=[1-9][0-9]*|q=[1-9][0-9]?|dpr=[1-9](?:\\.[0-9]+)?|auto=format)(?:&(?:[wh]=[1-9][0-9]*|q=[1-9][0-9]?|dpr=[1-9](?:\\.[0-9]+)?|auto=format))*)?$"
+ ]
+ }
+ ],
+ "resource_output_contract": {
+ "candidate_scope": "image_answer_fields",
+ "unexpected": "allow"
+ }
+ },
+ "artifact": "https://images.unsplash.com/photo-1493673272479-a20888bcee10?w=640&auto=format&q=80",
+ "ok": true,
+ "expected_task_pass": true,
+ "expected_metrics": {
+ "resource_pass": true
+ }
+ },
+ {
+ "id": "image-unsplash-detail-declared-display-equivalence-unlisted-query-rejected",
+ "category": "acceptance-contract",
+ "root_cause": "task_declared_image_display_equivalence",
+ "failure_mode": "false-positive",
+ "case": {
+ "url": "https://unsplash.com/photos/green-leaf-plants-mSK5nNsAsLY",
+ "track": "image",
+ "task_kind": "resource",
+ "content_score_applicable": false,
+ "expected_resources": [
+ {
+ "id": "main-image",
+ "kind": "image",
+ "urls": [
+ "https://images.unsplash.com/photo-1493673272479-a20888bcee10",
+ "https://unsplash.com/photos/mSK5nNsAsLY/download"
+ ],
+ "url_patterns": [
+ "^https://images\\.unsplash\\.com/photo\\-1493673272479\\-a20888bcee10(?:\\?(?:[wh]=[1-9][0-9]*|q=[1-9][0-9]?|dpr=[1-9](?:\\.[0-9]+)?|auto=format)(?:&(?:[wh]=[1-9][0-9]*|q=[1-9][0-9]?|dpr=[1-9](?:\\.[0-9]+)?|auto=format))*)?$"
+ ]
+ }
+ ],
+ "resource_output_contract": {
+ "candidate_scope": "image_answer_fields",
+ "unexpected": "allow"
+ }
+ },
+ "artifact": "https://images.unsplash.com/photo-1493673272479-a20888bcee10?w=640&auto=format&q=80&crop=unapproved",
+ "ok": true,
+ "expected_task_pass": false,
+ "expected_metrics": {
+ "resource_pass": false
+ }
+ },
+ {
+ "id": "image-pexels-detail-declared-display-equivalence-accepted",
+ "category": "acceptance-contract",
+ "root_cause": "task_declared_image_display_equivalence",
+ "failure_mode": "false-negative",
+ "case": {
+ "url": "https://www.pexels.com/photo/a-green-trees-in-the-forest-12377231/",
+ "track": "image",
+ "task_kind": "resource",
+ "content_score_applicable": false,
+ "expected_resources": [
+ {
+ "id": "main-image",
+ "kind": "image",
+ "urls": [
+ "https://images.pexels.com/photos/12377231/pexels-photo-12377231.jpeg"
+ ],
+ "url_patterns": [
+ "^https://images\\.pexels\\.com/photos/12377231/pexels\\-photo\\-12377231\\.jpeg(?:\\?(?:[wh]=[1-9][0-9]*|q=[1-9][0-9]?|dpr=[1-9](?:\\.[0-9]+)?|auto=compress|cs=tinysrgb)(?:&(?:[wh]=[1-9][0-9]*|q=[1-9][0-9]?|dpr=[1-9](?:\\.[0-9]+)?|auto=compress|cs=tinysrgb))*)?$"
+ ]
+ }
+ ],
+ "resource_output_contract": {
+ "candidate_scope": "image_answer_fields",
+ "unexpected": "allow"
+ }
+ },
+ "artifact": "https://images.pexels.com/photos/12377231/pexels-photo-12377231.jpeg?w=640&auto=compress&cs=tinysrgb",
+ "ok": true,
+ "expected_task_pass": true,
+ "expected_metrics": {
+ "resource_pass": true
+ }
+ },
+ {
+ "id": "image-pexels-detail-declared-display-equivalence-unlisted-query-rejected",
+ "category": "acceptance-contract",
+ "root_cause": "task_declared_image_display_equivalence",
+ "failure_mode": "false-positive",
+ "case": {
+ "url": "https://www.pexels.com/photo/a-green-trees-in-the-forest-12377231/",
+ "track": "image",
+ "task_kind": "resource",
+ "content_score_applicable": false,
+ "expected_resources": [
+ {
+ "id": "main-image",
+ "kind": "image",
+ "urls": [
+ "https://images.pexels.com/photos/12377231/pexels-photo-12377231.jpeg"
+ ],
+ "url_patterns": [
+ "^https://images\\.pexels\\.com/photos/12377231/pexels\\-photo\\-12377231\\.jpeg(?:\\?(?:[wh]=[1-9][0-9]*|q=[1-9][0-9]?|dpr=[1-9](?:\\.[0-9]+)?|auto=compress|cs=tinysrgb)(?:&(?:[wh]=[1-9][0-9]*|q=[1-9][0-9]?|dpr=[1-9](?:\\.[0-9]+)?|auto=compress|cs=tinysrgb))*)?$"
+ ]
+ }
+ ],
+ "resource_output_contract": {
+ "candidate_scope": "image_answer_fields",
+ "unexpected": "allow"
+ }
+ },
+ "artifact": "https://images.pexels.com/photos/12377231/pexels-photo-12377231.jpeg?w=640&auto=compress&cs=tinysrgb&crop=unapproved",
+ "ok": true,
+ "expected_task_pass": false,
+ "expected_metrics": {
+ "resource_pass": false
+ }
+ },
+ {
+ "id": "image-earth-main-declared-display-equivalence-accepted",
+ "category": "acceptance-contract",
+ "root_cause": "task_declared_image_display_equivalence",
+ "failure_mode": "false-negative",
+ "case": {
+ "url": "https://commons.wikimedia.org/wiki/File:The_Earth_seen_from_Apollo_17.jpg",
+ "track": "image",
+ "task_kind": "resource",
+ "content_score_applicable": false,
+ "expected_resources": [
+ {
+ "id": "main-image",
+ "kind": "image",
+ "urls": [
+ "https://thumb.wikimedia.org/wikipedia/commons/thumb/9/97/The_Earth_seen_from_Apollo_17.jpg/960px-The_Earth_seen_from_Apollo_17.jpg?utm_source=commons.wikimedia.org&utm_campaign=index&utm_content=thumbnail&_=20200925203546"
+ ],
+ "url_patterns": [
+ "^https://(?:upload|thumb)\\.wikimedia\\.org/wikipedia/commons/(?:thumb/)?9/97/The_Earth_seen_from_Apollo_17\\.jpg(?:/[1-9][0-9]*px-The_Earth_seen_from_Apollo_17\\.jpg)?(?:\\?(?:utm_source=(?:en\\.wikipedia\\.org|commons\\.wikimedia\\.org)|utm_campaign=(?:parser|index)|utm_content=thumbnail|_=[0-9]+)(?:&(?:utm_source=(?:en\\.wikipedia\\.org|commons\\.wikimedia\\.org)|utm_campaign=(?:parser|index)|utm_content=thumbnail|_=[0-9]+))*)?$"
+ ]
+ }
+ ],
+ "resource_output_contract": {
+ "candidate_scope": "image_answer_fields",
+ "unexpected": "allow"
+ }
+ },
+ "artifact": "https://upload.wikimedia.org/wikipedia/commons/thumb/9/97/The_Earth_seen_from_Apollo_17.jpg/640px-The_Earth_seen_from_Apollo_17.jpg",
+ "ok": true,
+ "expected_task_pass": true,
+ "expected_metrics": {
+ "resource_pass": true
+ }
+ },
+ {
+ "id": "image-earth-main-declared-display-equivalence-unlisted-query-rejected",
+ "category": "acceptance-contract",
+ "root_cause": "task_declared_image_display_equivalence",
+ "failure_mode": "false-positive",
+ "case": {
+ "url": "https://commons.wikimedia.org/wiki/File:The_Earth_seen_from_Apollo_17.jpg",
+ "track": "image",
+ "task_kind": "resource",
+ "content_score_applicable": false,
+ "expected_resources": [
+ {
+ "id": "main-image",
+ "kind": "image",
+ "urls": [
+ "https://thumb.wikimedia.org/wikipedia/commons/thumb/9/97/The_Earth_seen_from_Apollo_17.jpg/960px-The_Earth_seen_from_Apollo_17.jpg?utm_source=commons.wikimedia.org&utm_campaign=index&utm_content=thumbnail&_=20200925203546"
+ ],
+ "url_patterns": [
+ "^https://(?:upload|thumb)\\.wikimedia\\.org/wikipedia/commons/(?:thumb/)?9/97/The_Earth_seen_from_Apollo_17\\.jpg(?:/[1-9][0-9]*px-The_Earth_seen_from_Apollo_17\\.jpg)?(?:\\?(?:utm_source=(?:en\\.wikipedia\\.org|commons\\.wikimedia\\.org)|utm_campaign=(?:parser|index)|utm_content=thumbnail|_=[0-9]+)(?:&(?:utm_source=(?:en\\.wikipedia\\.org|commons\\.wikimedia\\.org)|utm_campaign=(?:parser|index)|utm_content=thumbnail|_=[0-9]+))*)?$"
+ ]
+ }
+ ],
+ "resource_output_contract": {
+ "candidate_scope": "image_answer_fields",
+ "unexpected": "allow"
+ }
+ },
+ "artifact": "https://upload.wikimedia.org/wikipedia/commons/thumb/9/97/The_Earth_seen_from_Apollo_17.jpg/640px-The_Earth_seen_from_Apollo_17.jpg?crop=unapproved",
+ "ok": true,
+ "expected_task_pass": false,
+ "expected_metrics": {
+ "resource_pass": false
+ }
}
],
"superseded_controls": {
diff --git a/harness/web_fetch/reference_lifecycle.py b/harness/web_fetch/reference_lifecycle.py
index 578e01864..35fd4ef7e 100644
--- a/harness/web_fetch/reference_lifecycle.py
+++ b/harness/web_fetch/reference_lifecycle.py
@@ -215,6 +215,56 @@ def _resource_target_matches(case, candidates: list[dict]) -> list[dict]:
return results
+def _resource_observation_scope(case, root, evidence):
+ """Retain an observation's old scope across an explicit score-only alias revision."""
+ current = resource_source_scope_identity(case)
+ binding = current['resource_output_contract'].pop('equivalence_revision', None)
+ if binding is None:
+ return current
+ identifier = safe_identifier(_get(case, 'id'))
+ if (not isinstance(binding, dict)
+ or binding.get('path') != 'resource-equivalence-revision.json'):
+ raise ReferenceLifecycleError('Invalid resource equivalence revision binding')
+ try:
+ raw = contained_path(root, identifier + '/' + binding['path']).read_bytes()
+ if sha256(raw) != binding.get('sha256'):
+ raise ReferenceLifecycleError('Resource equivalence revision hash changed')
+ receipt = json.loads(raw)
+ before = receipt['before_scope']
+ canonical = json.dumps(current, ensure_ascii=False, sort_keys=True, separators=(',', ':'))
+ if (receipt.get('kind') != 'offline_resource_equivalence_revision'
+ or receipt.get('provider_calls_added') != 0
+ or receipt.get('new_source_observation') is not False
+ or receipt.get('after_scope_sha256') != sha256(canonical)
+ or receipt.get('source_evidence_sha256') != _get(case, 'source_verification', {}).get('evidence_sha256')
+ or receipt.get('browser_observation_sha256') != evidence.get('independent_browser_observation', {}).get('sha256')
+ or not receipt.get('authority', {}).get('description')):
+ raise ReferenceLifecycleError('Resource equivalence revision authority/scope differs')
+ # Only add declared equivalents. Never replace primary source targets,
+ # their identity, candidate policy, task URL, or historical observations.
+ old_targets, new_targets = before['expected_resources'], current['expected_resources']
+ if ({k: v for k, v in before.items() if k != 'expected_resources'}
+ != {k: v for k, v in current.items() if k != 'expected_resources'}
+ or len(old_targets) != len(new_targets)):
+ raise ReferenceLifecycleError('Resource equivalence revision changed source scope')
+ for old, new in zip(old_targets, new_targets):
+ if ({k: v for k, v in old.items() if k not in {'aliases', 'url_patterns'}}
+ != {k: v for k, v in new.items() if k not in {'aliases', 'url_patterns'}}
+ or any(not set(old.get(key, [])) <= set(new.get(key, []))
+ for key in ('aliases', 'url_patterns'))):
+ raise ReferenceLifecycleError('Resource equivalence revision replaced a primary target')
+ from .scoring_regressions import require_scoring_regressions
+ controls = receipt.get('regression_ids')
+ if not isinstance(controls, list) or not controls:
+ raise ReferenceLifecycleError('Resource equivalence revision lacks executable controls')
+ require_scoring_regressions(controls)
+ except ReferenceLifecycleError:
+ raise
+ except (OSError, ValueError, TypeError, KeyError) as error:
+ raise ReferenceLifecycleError('Invalid resource equivalence revision') from error
+ return before
+
+
def validate_resource_source_evidence(case, dataset_path: Path) -> dict | None:
"""Fail closed unless every resource target has independent, hash-bound evidence.
@@ -268,6 +318,7 @@ def validate_resource_source_evidence(case, dataset_path: Path) -> dict | None:
candidates = [*candidates, *observed_candidates]
source_qualified = evidence.get("state") == "captured" and evidence.get("status") == 200
+ observed_scope = _resource_observation_scope(case, root, evidence)
browser_binding = evidence.get("independent_browser_observation")
if browser_binding is not None:
if (not isinstance(browser_binding, dict)
@@ -292,12 +343,13 @@ def validate_resource_source_evidence(case, dataset_path: Path) -> dict | None:
if (observation.get("case_id") != identifier
or observation.get("url") != _get(case, "url")
or observation.get("independent") is not True
- or observation.get("resource_scope_identity") != resource_source_scope_identity(case)
+ or observation.get("resource_scope_identity") != observed_scope
or not isinstance(observed_candidates, list) or len(observed_candidates) > 512
or observation.get("resource_candidates_sha256") != sha256(canonical)):
raise ReferenceLifecycleError(f"{identifier}: browser observation scope/hash mismatch")
timestamp(observation.get("checked_at"))
- browser_matches = _resource_target_matches(case, observed_candidates)
+ browser_matches = _resource_target_matches(
+ {**observed_scope, "id": observed_scope["case_id"]}, observed_candidates)
browser_qualified = (
observation.get("status") == "success"
and observation.get("completion", {}).get("satisfied") is True
diff --git a/tests/test_reference_lifecycle.py b/tests/test_reference_lifecycle.py
index efd5db3aa..bbf6c13cc 100644
--- a/tests/test_reference_lifecycle.py
+++ b/tests/test_reference_lifecycle.py
@@ -228,6 +228,59 @@ def test_resource_source_browser_observation_must_satisfy_all_targets(self):
with self.assertRaisesRegex(ReferenceLifecycleError, "does not cover every"):
validate_resource_source_evidence(case, self.dataset_path)
+ def test_resource_equivalence_keeps_original_browser_observation_and_primary_target(self):
+ from harness.web_fetch.browser_observations import match_resource_targets
+ original = self.resource_case()
+ scope = resource_source_scope_identity(original)
+ candidates = [{'tag': 'a', 'attribute': 'href', 'url': original.expected_resources[0]['urls'][0]}]
+ browser = {'schema_version': 1, 'case_id': original.id, 'url': original.url,
+ 'status': 'success', 'independent': True, 'checked_at': self.now.isoformat(),
+ 'resource_scope_identity': scope, 'resource_candidates': candidates,
+ 'resource_candidates_sha256': sha256(json.dumps(candidates, ensure_ascii=False,
+ sort_keys=True, separators=(',', ':'))),
+ 'target_matches': match_resource_targets(original, candidates),
+ 'completion': {'satisfied': True}}
+ original = self.resource_case(evidence_updates={'state': 'needs_review', 'status': 403,
+ 'resources': []}, browser=browser)
+ browser_path = self.root/'sources/resource/browser-observation.json'
+ browser_bytes = browser_path.read_bytes()
+ aliases = [{**original.expected_resources[0], 'aliases': ['https://cdn.example.test/a.png?w=800']}]
+ revised = replace(original, expected_resources=aliases)
+ new_scope = resource_source_scope_identity(revised)
+ receipt = {'kind': 'offline_resource_equivalence_revision', 'provider_calls_added': 0,
+ 'new_source_observation': False, 'before_scope': scope,
+ 'after_scope_sha256': sha256(json.dumps(new_scope, ensure_ascii=False,
+ sort_keys=True, separators=(',', ':'))),
+ 'source_evidence_sha256': original.source_verification['evidence_sha256'],
+ 'browser_observation_sha256': sha256(browser_bytes),
+ 'authority': {'description': 'Same observed asset; a score-only display-size equivalent.'},
+ 'regression_ids': ['resource-query-explicit-alias-preserved',
+ 'resource-query-different-resource-rejected']}
+ proof = self.root/'sources/resource/resource-equivalence-revision.json'
+ def bound_case(value):
+ raw = (json.dumps(value)+'\n').encode()
+ proof.write_bytes(raw)
+ return replace(revised, resource_output_contract={
+ **revised.resource_output_contract, 'equivalence_revision': {
+ 'path': proof.name, 'sha256': sha256(raw)}})
+ result = validate_resource_source_evidence(bound_case(receipt), self.dataset_path)
+ self.assertTrue(result['target_matches'][0]['satisfied'])
+ self.assertEqual(browser_path.read_bytes(), browser_bytes)
+ # A new alias cannot silently rewrite or replace the captured scope.
+ for defect in ('primary', 'policy', 'source', 'observation', 'controls', 'fresh'):
+ bad = deepcopy(receipt)
+ if defect == 'primary': bad['before_scope']['expected_resources'][0]['urls'] = ['https://wrong.test/other.png']
+ if defect == 'policy': bad['before_scope']['resource_output_contract'] = {'unexpected': 'reject'}
+ if defect == 'source': bad['source_evidence_sha256'] = '0'*64
+ if defect == 'observation': bad['browser_observation_sha256'] = '0'*64
+ if defect == 'controls': bad['regression_ids'] = []
+ if defect == 'fresh': bad['new_source_observation'] = True
+ with self.subTest(defect=defect), self.assertRaises(ReferenceLifecycleError):
+ validate_resource_source_evidence(bound_case(bad), self.dataset_path)
+ without_receipt = replace(revised, resource_output_contract={})
+ with self.assertRaisesRegex(ReferenceLifecycleError, 'scope/hash mismatch'):
+ validate_resource_source_evidence(without_receipt, self.dataset_path)
+
def test_per_case_rechecks_clock_and_immutable_scope(self):
binding = self.checks()["cases"]["body"]
with self.assertRaisesRegex(ReferenceLifecycleError, "30 minutes"):
From 052dc9c8321c9924725f5252624aaeb8c49506cc Mon Sep 17 00:00:00 2001
From: Maisiqi <324751762+Maisiqi@users.noreply.github.com>
Date: Fri, 16 Oct 2026 00:27:40 +0800
Subject: [PATCH 28/50] Reject invalid thumbnail host paths and retain
immutable rule proofs
---
evaluation/scoring-regressions.json | 156 +++++++++++++++++++++++
harness/web_fetch/reference_lifecycle.py | 3 +-
tests/test_reference_lifecycle.py | 5 +
3 files changed, 163 insertions(+), 1 deletion(-)
diff --git a/evaluation/scoring-regressions.json b/evaluation/scoring-regressions.json
index 2fb664fc6..105d32adf 100644
--- a/evaluation/scoring-regressions.json
+++ b/evaluation/scoring-regressions.json
@@ -32200,6 +32200,162 @@
"expected_metrics": {
"resource_pass": false
}
+ },
+ {
+ "id": "image-04-declared-display-real-thumbnail-accepted",
+ "category": "acceptance-contract",
+ "root_cause": "task_declared_thumbnail_host_path_coupling",
+ "failure_mode": "false-negative",
+ "case": {
+ "url": "https://en.wikipedia.org/wiki/Apollo_11",
+ "track": "image",
+ "task_kind": "resource",
+ "content_score_applicable": false,
+ "expected_resources": [
+ {
+ "id": "image-1",
+ "kind": "image-1",
+ "urls": [
+ "https://upload.wikimedia.org/wikipedia/commons/thumb/4/41/A_Man_on_the_Moon%2C_AS11-40-5903_%28cropped%29.jpg/330px-A_Man_on_the_Moon%2C_AS11-40-5903_%28cropped%29.jpg?utm_source=en.wikipedia.org&utm_campaign=parser&utm_content=thumbnail"
+ ],
+ "url_patterns": [
+ "^(?:https://upload\\.wikimedia\\.org/wikipedia/commons/4/41/A_Man_on_the_Moon,_AS11\\-40\\-5903_\\(cropped\\)\\.jpg|https://(?:upload|thumb)\\.wikimedia\\.org/wikipedia/commons/thumb/4/41/A_Man_on_the_Moon,_AS11\\-40\\-5903_\\(cropped\\)\\.jpg/[1-9][0-9]*px-A_Man_on_the_Moon,_AS11\\-40\\-5903_\\(cropped\\)\\.jpg)(?:\\?(?:utm_source=(?:en\\.wikipedia\\.org|commons\\.wikimedia\\.org)|utm_campaign=(?:parser|index)|utm_content=thumbnail|_=[0-9]+)(?:&(?:utm_source=(?:en\\.wikipedia\\.org|commons\\.wikimedia\\.org)|utm_campaign=(?:parser|index)|utm_content=thumbnail|_=[0-9]+))*)?$"
+ ]
+ },
+ {
+ "id": "image-2",
+ "kind": "image-2",
+ "urls": [
+ "https://upload.wikimedia.org/wikipedia/commons/thumb/2/27/Apollo_11_insignia.png/250px-Apollo_11_insignia.png?utm_source=en.wikipedia.org&utm_campaign=parser&utm_content=thumbnail"
+ ],
+ "url_patterns": [
+ "^(?:https://upload\\.wikimedia\\.org/wikipedia/commons/2/27/Apollo_11_insignia\\.png|https://(?:upload|thumb)\\.wikimedia\\.org/wikipedia/commons/thumb/2/27/Apollo_11_insignia\\.png/[1-9][0-9]*px-Apollo_11_insignia\\.png)(?:\\?(?:utm_source=(?:en\\.wikipedia\\.org|commons\\.wikimedia\\.org)|utm_campaign=(?:parser|index)|utm_content=thumbnail|_=[0-9]+)(?:&(?:utm_source=(?:en\\.wikipedia\\.org|commons\\.wikimedia\\.org)|utm_campaign=(?:parser|index)|utm_content=thumbnail|_=[0-9]+))*)?$"
+ ]
+ }
+ ],
+ "resource_output_contract": {
+ "candidate_scope": "image_answer_fields",
+ "unexpected": "allow"
+ }
+ },
+ "artifact": "https://thumb.wikimedia.org/wikipedia/commons/thumb/4/41/A_Man_on_the_Moon%2C_AS11-40-5903_%28cropped%29.jpg/640px-A_Man_on_the_Moon%2C_AS11-40-5903_%28cropped%29.jpg\nhttps://thumb.wikimedia.org/wikipedia/commons/thumb/2/27/Apollo_11_insignia.png/640px-Apollo_11_insignia.png",
+ "ok": true,
+ "expected_task_pass": true,
+ "expected_metrics": {
+ "resource_pass": true
+ }
+ },
+ {
+ "id": "image-04-declared-display-thumbnail-host-original-rejected",
+ "category": "acceptance-contract",
+ "root_cause": "task_declared_thumbnail_host_path_coupling",
+ "failure_mode": "false-positive",
+ "case": {
+ "url": "https://en.wikipedia.org/wiki/Apollo_11",
+ "track": "image",
+ "task_kind": "resource",
+ "content_score_applicable": false,
+ "expected_resources": [
+ {
+ "id": "image-1",
+ "kind": "image-1",
+ "urls": [
+ "https://upload.wikimedia.org/wikipedia/commons/thumb/4/41/A_Man_on_the_Moon%2C_AS11-40-5903_%28cropped%29.jpg/330px-A_Man_on_the_Moon%2C_AS11-40-5903_%28cropped%29.jpg?utm_source=en.wikipedia.org&utm_campaign=parser&utm_content=thumbnail"
+ ],
+ "url_patterns": [
+ "^(?:https://upload\\.wikimedia\\.org/wikipedia/commons/4/41/A_Man_on_the_Moon,_AS11\\-40\\-5903_\\(cropped\\)\\.jpg|https://(?:upload|thumb)\\.wikimedia\\.org/wikipedia/commons/thumb/4/41/A_Man_on_the_Moon,_AS11\\-40\\-5903_\\(cropped\\)\\.jpg/[1-9][0-9]*px-A_Man_on_the_Moon,_AS11\\-40\\-5903_\\(cropped\\)\\.jpg)(?:\\?(?:utm_source=(?:en\\.wikipedia\\.org|commons\\.wikimedia\\.org)|utm_campaign=(?:parser|index)|utm_content=thumbnail|_=[0-9]+)(?:&(?:utm_source=(?:en\\.wikipedia\\.org|commons\\.wikimedia\\.org)|utm_campaign=(?:parser|index)|utm_content=thumbnail|_=[0-9]+))*)?$"
+ ]
+ },
+ {
+ "id": "image-2",
+ "kind": "image-2",
+ "urls": [
+ "https://upload.wikimedia.org/wikipedia/commons/thumb/2/27/Apollo_11_insignia.png/250px-Apollo_11_insignia.png?utm_source=en.wikipedia.org&utm_campaign=parser&utm_content=thumbnail"
+ ],
+ "url_patterns": [
+ "^(?:https://upload\\.wikimedia\\.org/wikipedia/commons/2/27/Apollo_11_insignia\\.png|https://(?:upload|thumb)\\.wikimedia\\.org/wikipedia/commons/thumb/2/27/Apollo_11_insignia\\.png/[1-9][0-9]*px-Apollo_11_insignia\\.png)(?:\\?(?:utm_source=(?:en\\.wikipedia\\.org|commons\\.wikimedia\\.org)|utm_campaign=(?:parser|index)|utm_content=thumbnail|_=[0-9]+)(?:&(?:utm_source=(?:en\\.wikipedia\\.org|commons\\.wikimedia\\.org)|utm_campaign=(?:parser|index)|utm_content=thumbnail|_=[0-9]+))*)?$"
+ ]
+ }
+ ],
+ "resource_output_contract": {
+ "candidate_scope": "image_answer_fields",
+ "unexpected": "allow"
+ }
+ },
+ "artifact": "https://thumb.wikimedia.org/wikipedia/commons/4/41/A_Man_on_the_Moon%2C_AS11-40-5903_%28cropped%29.jpg\nhttps://thumb.wikimedia.org/wikipedia/commons/thumb/2/27/Apollo_11_insignia.png/640px-Apollo_11_insignia.png",
+ "ok": true,
+ "expected_task_pass": false,
+ "expected_metrics": {
+ "resource_pass": false
+ }
+ },
+ {
+ "id": "image-earth-main-declared-display-real-thumbnail-accepted",
+ "category": "acceptance-contract",
+ "root_cause": "task_declared_thumbnail_host_path_coupling",
+ "failure_mode": "false-negative",
+ "case": {
+ "url": "https://commons.wikimedia.org/wiki/File:The_Earth_seen_from_Apollo_17.jpg",
+ "track": "image",
+ "task_kind": "resource",
+ "content_score_applicable": false,
+ "expected_resources": [
+ {
+ "id": "main-image",
+ "kind": "image",
+ "urls": [
+ "https://thumb.wikimedia.org/wikipedia/commons/thumb/9/97/The_Earth_seen_from_Apollo_17.jpg/960px-The_Earth_seen_from_Apollo_17.jpg?utm_source=commons.wikimedia.org&utm_campaign=index&utm_content=thumbnail&_=20200925203546"
+ ],
+ "url_patterns": [
+ "^(?:https://upload\\.wikimedia\\.org/wikipedia/commons/9/97/The_Earth_seen_from_Apollo_17\\.jpg|https://(?:upload|thumb)\\.wikimedia\\.org/wikipedia/commons/thumb/9/97/The_Earth_seen_from_Apollo_17\\.jpg/[1-9][0-9]*px-The_Earth_seen_from_Apollo_17\\.jpg)(?:\\?(?:utm_source=(?:en\\.wikipedia\\.org|commons\\.wikimedia\\.org)|utm_campaign=(?:parser|index)|utm_content=thumbnail|_=[0-9]+)(?:&(?:utm_source=(?:en\\.wikipedia\\.org|commons\\.wikimedia\\.org)|utm_campaign=(?:parser|index)|utm_content=thumbnail|_=[0-9]+))*)?$"
+ ]
+ }
+ ],
+ "resource_output_contract": {
+ "candidate_scope": "image_answer_fields",
+ "unexpected": "allow"
+ }
+ },
+ "artifact": "https://thumb.wikimedia.org/wikipedia/commons/thumb/9/97/The_Earth_seen_from_Apollo_17.jpg/640px-The_Earth_seen_from_Apollo_17.jpg",
+ "ok": true,
+ "expected_task_pass": true,
+ "expected_metrics": {
+ "resource_pass": true
+ }
+ },
+ {
+ "id": "image-earth-main-declared-display-thumbnail-host-original-rejected",
+ "category": "acceptance-contract",
+ "root_cause": "task_declared_thumbnail_host_path_coupling",
+ "failure_mode": "false-positive",
+ "case": {
+ "url": "https://commons.wikimedia.org/wiki/File:The_Earth_seen_from_Apollo_17.jpg",
+ "track": "image",
+ "task_kind": "resource",
+ "content_score_applicable": false,
+ "expected_resources": [
+ {
+ "id": "main-image",
+ "kind": "image",
+ "urls": [
+ "https://thumb.wikimedia.org/wikipedia/commons/thumb/9/97/The_Earth_seen_from_Apollo_17.jpg/960px-The_Earth_seen_from_Apollo_17.jpg?utm_source=commons.wikimedia.org&utm_campaign=index&utm_content=thumbnail&_=20200925203546"
+ ],
+ "url_patterns": [
+ "^(?:https://upload\\.wikimedia\\.org/wikipedia/commons/9/97/The_Earth_seen_from_Apollo_17\\.jpg|https://(?:upload|thumb)\\.wikimedia\\.org/wikipedia/commons/thumb/9/97/The_Earth_seen_from_Apollo_17\\.jpg/[1-9][0-9]*px-The_Earth_seen_from_Apollo_17\\.jpg)(?:\\?(?:utm_source=(?:en\\.wikipedia\\.org|commons\\.wikimedia\\.org)|utm_campaign=(?:parser|index)|utm_content=thumbnail|_=[0-9]+)(?:&(?:utm_source=(?:en\\.wikipedia\\.org|commons\\.wikimedia\\.org)|utm_campaign=(?:parser|index)|utm_content=thumbnail|_=[0-9]+))*)?$"
+ ]
+ }
+ ],
+ "resource_output_contract": {
+ "candidate_scope": "image_answer_fields",
+ "unexpected": "allow"
+ }
+ },
+ "artifact": "https://thumb.wikimedia.org/wikipedia/commons/9/97/The_Earth_seen_from_Apollo_17.jpg",
+ "ok": true,
+ "expected_task_pass": false,
+ "expected_metrics": {
+ "resource_pass": false
+ }
}
],
"superseded_controls": {
diff --git a/harness/web_fetch/reference_lifecycle.py b/harness/web_fetch/reference_lifecycle.py
index 35fd4ef7e..efd9ca9b6 100644
--- a/harness/web_fetch/reference_lifecycle.py
+++ b/harness/web_fetch/reference_lifecycle.py
@@ -223,7 +223,8 @@ def _resource_observation_scope(case, root, evidence):
return current
identifier = safe_identifier(_get(case, 'id'))
if (not isinstance(binding, dict)
- or binding.get('path') != 'resource-equivalence-revision.json'):
+ or re.fullmatch(r'resource-equivalence-(?:revision|[0-9a-f]{64})\.json',
+ str(binding.get('path', ''))) is None):
raise ReferenceLifecycleError('Invalid resource equivalence revision binding')
try:
raw = contained_path(root, identifier + '/' + binding['path']).read_bytes()
diff --git a/tests/test_reference_lifecycle.py b/tests/test_reference_lifecycle.py
index bbf6c13cc..6ab6c9094 100644
--- a/tests/test_reference_lifecycle.py
+++ b/tests/test_reference_lifecycle.py
@@ -266,6 +266,11 @@ def bound_case(value):
result = validate_resource_source_evidence(bound_case(receipt), self.dataset_path)
self.assertTrue(result['target_matches'][0]['satisfied'])
self.assertEqual(browser_path.read_bytes(), browser_bytes)
+ # Later score-only revisions get immutable proof names, preserving prior receipts.
+ prior_proof = proof
+ proof = proof.with_name('resource-equivalence-'+sha256((json.dumps(receipt)+'\n').encode())+'.json')
+ self.assertIsNotNone(validate_resource_source_evidence(bound_case(receipt), self.dataset_path))
+ self.assertTrue(prior_proof.exists())
# A new alias cannot silently rewrite or replace the captured scope.
for defect in ('primary', 'policy', 'source', 'observation', 'controls', 'fresh'):
bad = deepcopy(receipt)
From e2a012e6bbd1829a4161256933d1fb8fefe62bc6 Mon Sep 17 00:00:00 2001
From: Maisiqi <324751762+Maisiqi@users.noreply.github.com>
Date: Fri, 16 Oct 2026 00:38:26 +0800
Subject: [PATCH 29/50] Decode HTML image URL attributes without collapsing
literal queries
---
evaluation/scoring-regressions.json | 90 +++++++++++++++++++++++++++++
harness/web_fetch/metrics.py | 11 ++--
2 files changed, 97 insertions(+), 4 deletions(-)
diff --git a/evaluation/scoring-regressions.json b/evaluation/scoring-regressions.json
index 105d32adf..4818743f8 100644
--- a/evaluation/scoring-regressions.json
+++ b/evaluation/scoring-regressions.json
@@ -32356,6 +32356,96 @@
"expected_metrics": {
"resource_pass": false
}
+ },
+ {
+ "id": "html-image-attribute-entities-decoded",
+ "category": "format-normalization",
+ "root_cause": "html_resource_attribute_entity_decoding",
+ "failure_mode": "false-negative",
+ "case": {
+ "url": "https://example.test/gallery",
+ "track": "image",
+ "task_kind": "resource",
+ "content_score_applicable": false,
+ "expected_resources": [
+ {
+ "id": "photo",
+ "urls": [
+ "https://cdn.example/photo.png?asset=one&size=640"
+ ]
+ }
+ ],
+ "resource_output_contract": {
+ "candidate_scope": "image_answer_fields",
+ "unexpected": "reject"
+ }
+ },
+ "artifact": "

",
+ "ok": true,
+ "expected_task_pass": true,
+ "expected_metrics": {
+ "resource_pass": true
+ }
+ },
+ {
+ "id": "plain-resource-query-entity-spelling-remains-literal",
+ "category": "format-normalization",
+ "root_cause": "html_resource_attribute_entity_decoding",
+ "failure_mode": "false-positive",
+ "case": {
+ "url": "https://example.test/gallery",
+ "track": "image",
+ "task_kind": "resource",
+ "content_score_applicable": false,
+ "expected_resources": [
+ {
+ "id": "photo",
+ "urls": [
+ "https://cdn.example/photo.png?asset=one&size=640"
+ ]
+ }
+ ],
+ "resource_output_contract": {
+ "candidate_scope": "image_answer_fields",
+ "unexpected": "reject"
+ }
+ },
+ "artifact": "https://cdn.example/photo.png?asset=one&size=640",
+ "ok": true,
+ "expected_task_pass": false,
+ "expected_metrics": {
+ "resource_pass": false
+ }
+ },
+ {
+ "id": "html-image-srcset-entities-decoded",
+ "category": "format-normalization",
+ "root_cause": "html_resource_attribute_entity_decoding",
+ "failure_mode": "false-negative",
+ "case": {
+ "url": "https://example.test/gallery",
+ "track": "image",
+ "task_kind": "resource",
+ "content_score_applicable": false,
+ "expected_resources": [
+ {
+ "id": "photo",
+ "urls": [
+ "https://cdn.example/photo.png?asset=one&size=640"
+ ]
+ }
+ ],
+ "resource_output_contract": {
+ "candidate_scope": "image_answer_fields",
+ "unexpected": "reject"
+ }
+ },
+ "artifact": "
![]()
",
+ "ok": true,
+ "expected_task_pass": true,
+ "expected_metrics": {
+ "resource_pass": true
+ }
}
],
"superseded_controls": {
diff --git a/harness/web_fetch/metrics.py b/harness/web_fetch/metrics.py
index e7710b8da..64d6e3441 100644
--- a/harness/web_fetch/metrics.py
+++ b/harness/web_fetch/metrics.py
@@ -143,20 +143,23 @@ def _looks_like_image_url(value: str) -> bool:
def _image_content_candidates(content: str) -> list[tuple[str, str]]:
candidates = [(url, "content.markdown_image")
for url in _markdown_image_destinations(content)]
- candidates.extend((match[1], "content.html_img") for match in re.findall(
+ candidates.extend((html.unescape(match[1]), "content.html_img") for match in re.findall(
r"
![]()
]*?\bsrc\s*=\s*([\"'])(.*?)\1", content, re.I | re.S
))
for match in re.findall(r"
![]()
]*?\bsrcset\s*=\s*([\"'])(.*?)\1", content, re.I | re.S):
- candidates.extend((part.strip().split()[0], "content.html_img_srcset")
+ candidates.extend((html.unescape(part.strip().split()[0]), "content.html_img_srcset")
for part in match[1].split(',') if part.strip())
+ # HTML attributes are decoded above. Do not also admit their raw entity
+ # spelling as a second bare candidate; plain URL answers remain literal.
+ bare_content = re.sub(r"
![]()
]*>", "", content, flags=re.I | re.S)
# Explicit image markup is typed evidence. A standalone HTTP URL may also
# be the entire answer, including an extensionless CDN address. Every URL
# with that syntax is admitted alike, without consulting target resources.
url_pattern = r"https?://[^\s<>'\")\]]+"
- bare_lines = {line.strip().strip('<>') for line in content.splitlines()
+ bare_lines = {line.strip().strip('<>') for line in bare_content.splitlines()
if re.fullmatch(r"\s*" + url_pattern + r">?\s*", line)}
candidates.extend((url, "content.bare_image_url") for url in
- re.findall(url_pattern, content)
+ re.findall(url_pattern, bare_content)
if _looks_like_image_url(url) or url in bare_lines)
return candidates
From d82eee2baa96be891e78ea7d04f1d7bb093dce4b Mon Sep 17 00:00:00 2001
From: Maisiqi <324751762+Maisiqi@users.noreply.github.com>
Date: Fri, 16 Oct 2026 00:48:41 +0800
Subject: [PATCH 30/50] Cover declared photo display variants without ignoring
arbitrary queries
---
evaluation/scoring-regressions.json | 292 ++++++++++++++++++++++++++++
1 file changed, 292 insertions(+)
diff --git a/evaluation/scoring-regressions.json b/evaluation/scoring-regressions.json
index 4818743f8..9a86ff66b 100644
--- a/evaluation/scoring-regressions.json
+++ b/evaluation/scoring-regressions.json
@@ -32446,6 +32446,298 @@
"expected_metrics": {
"resource_pass": true
}
+ },
+ {
+ "id": "image-unsplash-detail-documented-same-photo-display-accepted",
+ "category": "acceptance-contract",
+ "root_cause": "declared_photo_display_and_download_variants",
+ "failure_mode": "false-negative",
+ "case": {
+ "url": "https://unsplash.com/photos/green-leaf-plants-mSK5nNsAsLY",
+ "track": "image",
+ "task_kind": "resource",
+ "content_score_applicable": false,
+ "expected_resources": [
+ {
+ "id": "main-image",
+ "kind": "image",
+ "urls": [
+ "https://images.unsplash.com/photo-1493673272479-a20888bcee10",
+ "https://unsplash.com/photos/mSK5nNsAsLY/download"
+ ],
+ "url_patterns": [
+ "^https://images\\.unsplash\\.com/photo\\-1493673272479\\-a20888bcee10(?:\\?(?:[wh]=[1-9][0-9]*|q=[1-9][0-9]?|dpr=[1-9](?:\\.[0-9]+)?|auto=format)(?:&(?:[wh]=[1-9][0-9]*|q=[1-9][0-9]?|dpr=[1-9](?:\\.[0-9]+)?|auto=format))*)?$",
+ "^https://images\\.unsplash\\.com/photo\\-1493673272479\\-a20888bcee10(?:\\?(?:[wh]=[1-9][0-9]*|q=(?:[1-9][0-9]?|100)|dpr=[1-9](?:\\.[0-9]+)?|auto=format|fm=(?:jpg|jpeg|png|webp)|cs=(?:srgb|tinysrgb)|ixlib=[A-Za-z0-9_.-]+|ixid=(?:[A-Za-z0-9_.~-]|%[0-9A-Fa-f]{2})+)(?:&(?:[wh]=[1-9][0-9]*|q=(?:[1-9][0-9]?|100)|dpr=[1-9](?:\\.[0-9]+)?|auto=format|fm=(?:jpg|jpeg|png|webp)|cs=(?:srgb|tinysrgb)|ixlib=[A-Za-z0-9_.-]+|ixid=(?:[A-Za-z0-9_.~-]|%[0-9A-Fa-f]{2})+))*)?$",
+ "^https://images\\.unsplash\\.com/photo\\-1493673272479\\-a20888bcee10(?:\\?(?:w=[1-9][0-9]*|fit=(?:crop|max|clip)|q=(?:[1-9][0-9]?|100)|dpr=[1-9](?:\\.[0-9]+)?|auto=format|fm=(?:jpg|jpeg|png|webp)|cs=(?:srgb|tinysrgb)|ixlib=[A-Za-z0-9_.-]+|ixid=(?:[A-Za-z0-9_.~-]|%[0-9A-Fa-f]{2})+)(?:&(?:w=[1-9][0-9]*|fit=(?:crop|max|clip)|q=(?:[1-9][0-9]?|100)|dpr=[1-9](?:\\.[0-9]+)?|auto=format|fm=(?:jpg|jpeg|png|webp)|cs=(?:srgb|tinysrgb)|ixlib=[A-Za-z0-9_.-]+|ixid=(?:[A-Za-z0-9_.~-]|%[0-9A-Fa-f]{2})+))*)?$"
+ ]
+ }
+ ],
+ "resource_output_contract": {
+ "candidate_scope": "image_answer_fields",
+ "unexpected": "allow"
+ }
+ },
+ "artifact": "https://images.unsplash.com/photo-1493673272479-a20888bcee10?fm=jpg&q=80&w=1080&auto=format&fit=crop&ixlib=rb-4.1.0&ixid=example%3D",
+ "ok": true,
+ "expected_task_pass": true,
+ "expected_metrics": {
+ "resource_pass": true
+ }
+ },
+ {
+ "id": "image-unsplash-detail-documented-undeclared-region-rejected",
+ "category": "acceptance-contract",
+ "root_cause": "declared_photo_display_and_download_variants",
+ "failure_mode": "false-positive",
+ "case": {
+ "url": "https://unsplash.com/photos/green-leaf-plants-mSK5nNsAsLY",
+ "track": "image",
+ "task_kind": "resource",
+ "content_score_applicable": false,
+ "expected_resources": [
+ {
+ "id": "main-image",
+ "kind": "image",
+ "urls": [
+ "https://images.unsplash.com/photo-1493673272479-a20888bcee10",
+ "https://unsplash.com/photos/mSK5nNsAsLY/download"
+ ],
+ "url_patterns": [
+ "^https://images\\.unsplash\\.com/photo\\-1493673272479\\-a20888bcee10(?:\\?(?:[wh]=[1-9][0-9]*|q=[1-9][0-9]?|dpr=[1-9](?:\\.[0-9]+)?|auto=format)(?:&(?:[wh]=[1-9][0-9]*|q=[1-9][0-9]?|dpr=[1-9](?:\\.[0-9]+)?|auto=format))*)?$",
+ "^https://images\\.unsplash\\.com/photo\\-1493673272479\\-a20888bcee10(?:\\?(?:[wh]=[1-9][0-9]*|q=(?:[1-9][0-9]?|100)|dpr=[1-9](?:\\.[0-9]+)?|auto=format|fm=(?:jpg|jpeg|png|webp)|cs=(?:srgb|tinysrgb)|ixlib=[A-Za-z0-9_.-]+|ixid=(?:[A-Za-z0-9_.~-]|%[0-9A-Fa-f]{2})+)(?:&(?:[wh]=[1-9][0-9]*|q=(?:[1-9][0-9]?|100)|dpr=[1-9](?:\\.[0-9]+)?|auto=format|fm=(?:jpg|jpeg|png|webp)|cs=(?:srgb|tinysrgb)|ixlib=[A-Za-z0-9_.-]+|ixid=(?:[A-Za-z0-9_.~-]|%[0-9A-Fa-f]{2})+))*)?$",
+ "^https://images\\.unsplash\\.com/photo\\-1493673272479\\-a20888bcee10(?:\\?(?:w=[1-9][0-9]*|fit=(?:crop|max|clip)|q=(?:[1-9][0-9]?|100)|dpr=[1-9](?:\\.[0-9]+)?|auto=format|fm=(?:jpg|jpeg|png|webp)|cs=(?:srgb|tinysrgb)|ixlib=[A-Za-z0-9_.-]+|ixid=(?:[A-Za-z0-9_.~-]|%[0-9A-Fa-f]{2})+)(?:&(?:w=[1-9][0-9]*|fit=(?:crop|max|clip)|q=(?:[1-9][0-9]?|100)|dpr=[1-9](?:\\.[0-9]+)?|auto=format|fm=(?:jpg|jpeg|png|webp)|cs=(?:srgb|tinysrgb)|ixlib=[A-Za-z0-9_.-]+|ixid=(?:[A-Za-z0-9_.~-]|%[0-9A-Fa-f]{2})+))*)?$"
+ ]
+ }
+ ],
+ "resource_output_contract": {
+ "candidate_scope": "image_answer_fields",
+ "unexpected": "allow"
+ }
+ },
+ "artifact": "https://images.unsplash.com/photo-1493673272479-a20888bcee10?fm=jpg&q=80&w=1080&auto=format&fit=crop&ixlib=rb-4.1.0&ixid=example%3D&h=500&crop=left",
+ "ok": true,
+ "expected_task_pass": false,
+ "expected_metrics": {
+ "resource_pass": false
+ }
+ },
+ {
+ "id": "image-unsplash-detail-documented-unverified-signature-rejected",
+ "category": "acceptance-contract",
+ "root_cause": "declared_photo_display_and_download_variants",
+ "failure_mode": "false-positive",
+ "case": {
+ "url": "https://unsplash.com/photos/green-leaf-plants-mSK5nNsAsLY",
+ "track": "image",
+ "task_kind": "resource",
+ "content_score_applicable": false,
+ "expected_resources": [
+ {
+ "id": "main-image",
+ "kind": "image",
+ "urls": [
+ "https://images.unsplash.com/photo-1493673272479-a20888bcee10",
+ "https://unsplash.com/photos/mSK5nNsAsLY/download"
+ ],
+ "url_patterns": [
+ "^https://images\\.unsplash\\.com/photo\\-1493673272479\\-a20888bcee10(?:\\?(?:[wh]=[1-9][0-9]*|q=[1-9][0-9]?|dpr=[1-9](?:\\.[0-9]+)?|auto=format)(?:&(?:[wh]=[1-9][0-9]*|q=[1-9][0-9]?|dpr=[1-9](?:\\.[0-9]+)?|auto=format))*)?$",
+ "^https://images\\.unsplash\\.com/photo\\-1493673272479\\-a20888bcee10(?:\\?(?:[wh]=[1-9][0-9]*|q=(?:[1-9][0-9]?|100)|dpr=[1-9](?:\\.[0-9]+)?|auto=format|fm=(?:jpg|jpeg|png|webp)|cs=(?:srgb|tinysrgb)|ixlib=[A-Za-z0-9_.-]+|ixid=(?:[A-Za-z0-9_.~-]|%[0-9A-Fa-f]{2})+)(?:&(?:[wh]=[1-9][0-9]*|q=(?:[1-9][0-9]?|100)|dpr=[1-9](?:\\.[0-9]+)?|auto=format|fm=(?:jpg|jpeg|png|webp)|cs=(?:srgb|tinysrgb)|ixlib=[A-Za-z0-9_.-]+|ixid=(?:[A-Za-z0-9_.~-]|%[0-9A-Fa-f]{2})+))*)?$",
+ "^https://images\\.unsplash\\.com/photo\\-1493673272479\\-a20888bcee10(?:\\?(?:w=[1-9][0-9]*|fit=(?:crop|max|clip)|q=(?:[1-9][0-9]?|100)|dpr=[1-9](?:\\.[0-9]+)?|auto=format|fm=(?:jpg|jpeg|png|webp)|cs=(?:srgb|tinysrgb)|ixlib=[A-Za-z0-9_.-]+|ixid=(?:[A-Za-z0-9_.~-]|%[0-9A-Fa-f]{2})+)(?:&(?:w=[1-9][0-9]*|fit=(?:crop|max|clip)|q=(?:[1-9][0-9]?|100)|dpr=[1-9](?:\\.[0-9]+)?|auto=format|fm=(?:jpg|jpeg|png|webp)|cs=(?:srgb|tinysrgb)|ixlib=[A-Za-z0-9_.-]+|ixid=(?:[A-Za-z0-9_.~-]|%[0-9A-Fa-f]{2})+))*)?$"
+ ]
+ }
+ ],
+ "resource_output_contract": {
+ "candidate_scope": "image_answer_fields",
+ "unexpected": "allow"
+ }
+ },
+ "artifact": "https://images.unsplash.com/photo-1493673272479-a20888bcee10?fm=jpg&q=80&w=1080&auto=format&fit=crop&ixlib=rb-4.1.0&ixid=example%3D&s=unverified",
+ "ok": true,
+ "expected_task_pass": false,
+ "expected_metrics": {
+ "resource_pass": false
+ }
+ },
+ {
+ "id": "image-unsplash-detail-documented-different-photo-rejected",
+ "category": "acceptance-contract",
+ "root_cause": "declared_photo_display_and_download_variants",
+ "failure_mode": "false-positive",
+ "case": {
+ "url": "https://unsplash.com/photos/green-leaf-plants-mSK5nNsAsLY",
+ "track": "image",
+ "task_kind": "resource",
+ "content_score_applicable": false,
+ "expected_resources": [
+ {
+ "id": "main-image",
+ "kind": "image",
+ "urls": [
+ "https://images.unsplash.com/photo-1493673272479-a20888bcee10",
+ "https://unsplash.com/photos/mSK5nNsAsLY/download"
+ ],
+ "url_patterns": [
+ "^https://images\\.unsplash\\.com/photo\\-1493673272479\\-a20888bcee10(?:\\?(?:[wh]=[1-9][0-9]*|q=[1-9][0-9]?|dpr=[1-9](?:\\.[0-9]+)?|auto=format)(?:&(?:[wh]=[1-9][0-9]*|q=[1-9][0-9]?|dpr=[1-9](?:\\.[0-9]+)?|auto=format))*)?$",
+ "^https://images\\.unsplash\\.com/photo\\-1493673272479\\-a20888bcee10(?:\\?(?:[wh]=[1-9][0-9]*|q=(?:[1-9][0-9]?|100)|dpr=[1-9](?:\\.[0-9]+)?|auto=format|fm=(?:jpg|jpeg|png|webp)|cs=(?:srgb|tinysrgb)|ixlib=[A-Za-z0-9_.-]+|ixid=(?:[A-Za-z0-9_.~-]|%[0-9A-Fa-f]{2})+)(?:&(?:[wh]=[1-9][0-9]*|q=(?:[1-9][0-9]?|100)|dpr=[1-9](?:\\.[0-9]+)?|auto=format|fm=(?:jpg|jpeg|png|webp)|cs=(?:srgb|tinysrgb)|ixlib=[A-Za-z0-9_.-]+|ixid=(?:[A-Za-z0-9_.~-]|%[0-9A-Fa-f]{2})+))*)?$",
+ "^https://images\\.unsplash\\.com/photo\\-1493673272479\\-a20888bcee10(?:\\?(?:w=[1-9][0-9]*|fit=(?:crop|max|clip)|q=(?:[1-9][0-9]?|100)|dpr=[1-9](?:\\.[0-9]+)?|auto=format|fm=(?:jpg|jpeg|png|webp)|cs=(?:srgb|tinysrgb)|ixlib=[A-Za-z0-9_.-]+|ixid=(?:[A-Za-z0-9_.~-]|%[0-9A-Fa-f]{2})+)(?:&(?:w=[1-9][0-9]*|fit=(?:crop|max|clip)|q=(?:[1-9][0-9]?|100)|dpr=[1-9](?:\\.[0-9]+)?|auto=format|fm=(?:jpg|jpeg|png|webp)|cs=(?:srgb|tinysrgb)|ixlib=[A-Za-z0-9_.-]+|ixid=(?:[A-Za-z0-9_.~-]|%[0-9A-Fa-f]{2})+))*)?$"
+ ]
+ }
+ ],
+ "resource_output_contract": {
+ "candidate_scope": "image_answer_fields",
+ "unexpected": "allow"
+ }
+ },
+ "artifact": "https://images.unsplash.com/photo-0000000000000-different?fm=jpg&q=80&w=1080&auto=format&fit=crop&ixlib=rb-4.1.0&ixid=example%3D",
+ "ok": true,
+ "expected_task_pass": false,
+ "expected_metrics": {
+ "resource_pass": false
+ }
+ },
+ {
+ "id": "image-pexels-detail-documented-same-photo-display-accepted",
+ "category": "acceptance-contract",
+ "root_cause": "declared_photo_display_and_download_variants",
+ "failure_mode": "false-negative",
+ "case": {
+ "url": "https://www.pexels.com/photo/a-green-trees-in-the-forest-12377231/",
+ "track": "image",
+ "task_kind": "resource",
+ "content_score_applicable": false,
+ "expected_resources": [
+ {
+ "id": "main-image",
+ "kind": "image",
+ "urls": [
+ "https://images.pexels.com/photos/12377231/pexels-photo-12377231.jpeg"
+ ],
+ "url_patterns": [
+ "^https://images\\.pexels\\.com/photos/12377231/pexels\\-photo\\-12377231\\.jpeg(?:\\?(?:[wh]=[1-9][0-9]*|q=[1-9][0-9]?|dpr=[1-9](?:\\.[0-9]+)?|auto=compress|cs=tinysrgb)(?:&(?:[wh]=[1-9][0-9]*|q=[1-9][0-9]?|dpr=[1-9](?:\\.[0-9]+)?|auto=compress|cs=tinysrgb))*)?$",
+ "^https://images\\.pexels\\.com/photos/12377231/pexels\\-photo\\-12377231\\.jpeg(?:\\?(?:[wh]=[1-9][0-9]*|q=(?:[1-9][0-9]?|100)|dpr=[1-9](?:\\.[0-9]+)?|auto=compress|fm=(?:jpg|jpeg|png|webp)|cs=(?:srgb|tinysrgb)|dl=[A-Za-z0-9_.%-]+)(?:&(?:[wh]=[1-9][0-9]*|q=(?:[1-9][0-9]?|100)|dpr=[1-9](?:\\.[0-9]+)?|auto=compress|fm=(?:jpg|jpeg|png|webp)|cs=(?:srgb|tinysrgb)|dl=[A-Za-z0-9_.%-]+))*)?$",
+ "^https://images\\.pexels\\.com/photos/12377231/pexels\\-photo\\-12377231\\.jpeg(?:\\?(?:w=[1-9][0-9]*|fit=(?:crop|max|clip)|q=(?:[1-9][0-9]?|100)|dpr=[1-9](?:\\.[0-9]+)?|auto=compress|fm=(?:jpg|jpeg|png|webp)|cs=(?:srgb|tinysrgb)|dl=[A-Za-z0-9_.%-]+)(?:&(?:w=[1-9][0-9]*|fit=(?:crop|max|clip)|q=(?:[1-9][0-9]?|100)|dpr=[1-9](?:\\.[0-9]+)?|auto=compress|fm=(?:jpg|jpeg|png|webp)|cs=(?:srgb|tinysrgb)|dl=[A-Za-z0-9_.%-]+))*)?$"
+ ]
+ }
+ ],
+ "resource_output_contract": {
+ "candidate_scope": "image_answer_fields",
+ "unexpected": "allow"
+ }
+ },
+ "artifact": "https://images.pexels.com/photos/12377231/pexels-photo-12377231.jpeg?cs=srgb&dl=download.jpg&fm=jpg",
+ "ok": true,
+ "expected_task_pass": true,
+ "expected_metrics": {
+ "resource_pass": true
+ }
+ },
+ {
+ "id": "image-pexels-detail-documented-undeclared-region-rejected",
+ "category": "acceptance-contract",
+ "root_cause": "declared_photo_display_and_download_variants",
+ "failure_mode": "false-positive",
+ "case": {
+ "url": "https://www.pexels.com/photo/a-green-trees-in-the-forest-12377231/",
+ "track": "image",
+ "task_kind": "resource",
+ "content_score_applicable": false,
+ "expected_resources": [
+ {
+ "id": "main-image",
+ "kind": "image",
+ "urls": [
+ "https://images.pexels.com/photos/12377231/pexels-photo-12377231.jpeg"
+ ],
+ "url_patterns": [
+ "^https://images\\.pexels\\.com/photos/12377231/pexels\\-photo\\-12377231\\.jpeg(?:\\?(?:[wh]=[1-9][0-9]*|q=[1-9][0-9]?|dpr=[1-9](?:\\.[0-9]+)?|auto=compress|cs=tinysrgb)(?:&(?:[wh]=[1-9][0-9]*|q=[1-9][0-9]?|dpr=[1-9](?:\\.[0-9]+)?|auto=compress|cs=tinysrgb))*)?$",
+ "^https://images\\.pexels\\.com/photos/12377231/pexels\\-photo\\-12377231\\.jpeg(?:\\?(?:[wh]=[1-9][0-9]*|q=(?:[1-9][0-9]?|100)|dpr=[1-9](?:\\.[0-9]+)?|auto=compress|fm=(?:jpg|jpeg|png|webp)|cs=(?:srgb|tinysrgb)|dl=[A-Za-z0-9_.%-]+)(?:&(?:[wh]=[1-9][0-9]*|q=(?:[1-9][0-9]?|100)|dpr=[1-9](?:\\.[0-9]+)?|auto=compress|fm=(?:jpg|jpeg|png|webp)|cs=(?:srgb|tinysrgb)|dl=[A-Za-z0-9_.%-]+))*)?$",
+ "^https://images\\.pexels\\.com/photos/12377231/pexels\\-photo\\-12377231\\.jpeg(?:\\?(?:w=[1-9][0-9]*|fit=(?:crop|max|clip)|q=(?:[1-9][0-9]?|100)|dpr=[1-9](?:\\.[0-9]+)?|auto=compress|fm=(?:jpg|jpeg|png|webp)|cs=(?:srgb|tinysrgb)|dl=[A-Za-z0-9_.%-]+)(?:&(?:w=[1-9][0-9]*|fit=(?:crop|max|clip)|q=(?:[1-9][0-9]?|100)|dpr=[1-9](?:\\.[0-9]+)?|auto=compress|fm=(?:jpg|jpeg|png|webp)|cs=(?:srgb|tinysrgb)|dl=[A-Za-z0-9_.%-]+))*)?$"
+ ]
+ }
+ ],
+ "resource_output_contract": {
+ "candidate_scope": "image_answer_fields",
+ "unexpected": "allow"
+ }
+ },
+ "artifact": "https://images.pexels.com/photos/12377231/pexels-photo-12377231.jpeg?cs=srgb&dl=download.jpg&fm=jpg&crop=left",
+ "ok": true,
+ "expected_task_pass": false,
+ "expected_metrics": {
+ "resource_pass": false
+ }
+ },
+ {
+ "id": "image-pexels-detail-documented-unverified-signature-rejected",
+ "category": "acceptance-contract",
+ "root_cause": "declared_photo_display_and_download_variants",
+ "failure_mode": "false-positive",
+ "case": {
+ "url": "https://www.pexels.com/photo/a-green-trees-in-the-forest-12377231/",
+ "track": "image",
+ "task_kind": "resource",
+ "content_score_applicable": false,
+ "expected_resources": [
+ {
+ "id": "main-image",
+ "kind": "image",
+ "urls": [
+ "https://images.pexels.com/photos/12377231/pexels-photo-12377231.jpeg"
+ ],
+ "url_patterns": [
+ "^https://images\\.pexels\\.com/photos/12377231/pexels\\-photo\\-12377231\\.jpeg(?:\\?(?:[wh]=[1-9][0-9]*|q=[1-9][0-9]?|dpr=[1-9](?:\\.[0-9]+)?|auto=compress|cs=tinysrgb)(?:&(?:[wh]=[1-9][0-9]*|q=[1-9][0-9]?|dpr=[1-9](?:\\.[0-9]+)?|auto=compress|cs=tinysrgb))*)?$",
+ "^https://images\\.pexels\\.com/photos/12377231/pexels\\-photo\\-12377231\\.jpeg(?:\\?(?:[wh]=[1-9][0-9]*|q=(?:[1-9][0-9]?|100)|dpr=[1-9](?:\\.[0-9]+)?|auto=compress|fm=(?:jpg|jpeg|png|webp)|cs=(?:srgb|tinysrgb)|dl=[A-Za-z0-9_.%-]+)(?:&(?:[wh]=[1-9][0-9]*|q=(?:[1-9][0-9]?|100)|dpr=[1-9](?:\\.[0-9]+)?|auto=compress|fm=(?:jpg|jpeg|png|webp)|cs=(?:srgb|tinysrgb)|dl=[A-Za-z0-9_.%-]+))*)?$",
+ "^https://images\\.pexels\\.com/photos/12377231/pexels\\-photo\\-12377231\\.jpeg(?:\\?(?:w=[1-9][0-9]*|fit=(?:crop|max|clip)|q=(?:[1-9][0-9]?|100)|dpr=[1-9](?:\\.[0-9]+)?|auto=compress|fm=(?:jpg|jpeg|png|webp)|cs=(?:srgb|tinysrgb)|dl=[A-Za-z0-9_.%-]+)(?:&(?:w=[1-9][0-9]*|fit=(?:crop|max|clip)|q=(?:[1-9][0-9]?|100)|dpr=[1-9](?:\\.[0-9]+)?|auto=compress|fm=(?:jpg|jpeg|png|webp)|cs=(?:srgb|tinysrgb)|dl=[A-Za-z0-9_.%-]+))*)?$"
+ ]
+ }
+ ],
+ "resource_output_contract": {
+ "candidate_scope": "image_answer_fields",
+ "unexpected": "allow"
+ }
+ },
+ "artifact": "https://images.pexels.com/photos/12377231/pexels-photo-12377231.jpeg?cs=srgb&dl=download.jpg&fm=jpg&s=unverified",
+ "ok": true,
+ "expected_task_pass": false,
+ "expected_metrics": {
+ "resource_pass": false
+ }
+ },
+ {
+ "id": "image-pexels-detail-documented-different-photo-rejected",
+ "category": "acceptance-contract",
+ "root_cause": "declared_photo_display_and_download_variants",
+ "failure_mode": "false-positive",
+ "case": {
+ "url": "https://www.pexels.com/photo/a-green-trees-in-the-forest-12377231/",
+ "track": "image",
+ "task_kind": "resource",
+ "content_score_applicable": false,
+ "expected_resources": [
+ {
+ "id": "main-image",
+ "kind": "image",
+ "urls": [
+ "https://images.pexels.com/photos/12377231/pexels-photo-12377231.jpeg"
+ ],
+ "url_patterns": [
+ "^https://images\\.pexels\\.com/photos/12377231/pexels\\-photo\\-12377231\\.jpeg(?:\\?(?:[wh]=[1-9][0-9]*|q=[1-9][0-9]?|dpr=[1-9](?:\\.[0-9]+)?|auto=compress|cs=tinysrgb)(?:&(?:[wh]=[1-9][0-9]*|q=[1-9][0-9]?|dpr=[1-9](?:\\.[0-9]+)?|auto=compress|cs=tinysrgb))*)?$",
+ "^https://images\\.pexels\\.com/photos/12377231/pexels\\-photo\\-12377231\\.jpeg(?:\\?(?:[wh]=[1-9][0-9]*|q=(?:[1-9][0-9]?|100)|dpr=[1-9](?:\\.[0-9]+)?|auto=compress|fm=(?:jpg|jpeg|png|webp)|cs=(?:srgb|tinysrgb)|dl=[A-Za-z0-9_.%-]+)(?:&(?:[wh]=[1-9][0-9]*|q=(?:[1-9][0-9]?|100)|dpr=[1-9](?:\\.[0-9]+)?|auto=compress|fm=(?:jpg|jpeg|png|webp)|cs=(?:srgb|tinysrgb)|dl=[A-Za-z0-9_.%-]+))*)?$",
+ "^https://images\\.pexels\\.com/photos/12377231/pexels\\-photo\\-12377231\\.jpeg(?:\\?(?:w=[1-9][0-9]*|fit=(?:crop|max|clip)|q=(?:[1-9][0-9]?|100)|dpr=[1-9](?:\\.[0-9]+)?|auto=compress|fm=(?:jpg|jpeg|png|webp)|cs=(?:srgb|tinysrgb)|dl=[A-Za-z0-9_.%-]+)(?:&(?:w=[1-9][0-9]*|fit=(?:crop|max|clip)|q=(?:[1-9][0-9]?|100)|dpr=[1-9](?:\\.[0-9]+)?|auto=compress|fm=(?:jpg|jpeg|png|webp)|cs=(?:srgb|tinysrgb)|dl=[A-Za-z0-9_.%-]+))*)?$"
+ ]
+ }
+ ],
+ "resource_output_contract": {
+ "candidate_scope": "image_answer_fields",
+ "unexpected": "allow"
+ }
+ },
+ "artifact": "https://images.pexels.com/photos/99999999/pexels-photo-99999999.jpeg?cs=srgb&dl=download.jpg&fm=jpg",
+ "ok": true,
+ "expected_task_pass": false,
+ "expected_metrics": {
+ "resource_pass": false
+ }
}
],
"superseded_controls": {
From 5c7519940ebbf5a3c7f1757c57fc7a86210adca8 Mon Sep 17 00:00:00 2001
From: Maisiqi <324751762+Maisiqi@users.noreply.github.com>
Date: Fri, 16 Oct 2026 01:03:38 +0800
Subject: [PATCH 31/50] Report known-outcome evidence audits separately from
blind review
---
.agents/skills/benchmark-report/SKILL.md | 2 +-
harness/web_fetch/report.py | 13 ++++--
tests/test_review_protocol_presentation.py | 49 ++++++++++++++++++++++
3 files changed, 60 insertions(+), 4 deletions(-)
create mode 100644 tests/test_review_protocol_presentation.py
diff --git a/.agents/skills/benchmark-report/SKILL.md b/.agents/skills/benchmark-report/SKILL.md
index 230200473..7d3b79403 100644
--- a/.agents/skills/benchmark-report/SKILL.md
+++ b/.agents/skills/benchmark-report/SKILL.md
@@ -25,7 +25,7 @@ Public report CLIs default to English where supported and expose `--language en|
6. For heterogeneous same-window latency evidence, preserve the full predeclared task-by-method matrix. Show all-call latency on that common denominator separately from successful-call latency on each method's conditional subset. Treat the all-method shared-pass intersection as a diagnostic, never as a post-outcome selector or ranking sample.
7. At aggregate and publication boundaries, copy only explicit hash-bound evidence and public audit files. Reject symlinks and exclude worker trees, host paths, credentials, and internal run logs. Verify primary, auxiliary, delivered-file, and retained retry artifacts before release.
When a saved-input handoff needs host-metadata redaction, use the shared publication privacy owner. Preserve private originals and distinguish original from public file hashes; rebind dependent metadata references and disclose auxiliary debug-log projections. Never rewrite scored answers, references or response payloads as a privacy repair. Verify unchanged verdicts, timings and review decisions, and rebuild from the public inputs. Keep regenerated package/report wrappers outside the original-input projection receipt so normal rebuilding does not invalidate it or create circular hashes.
-8. Independent result review is opt-in: do not start it unless the user explicitly requests review. When requested without a broader scope, review machine failures only; review machine passes or every attempt only when the user explicitly asks for full review. Keep the initial verdict blind, but do not equate agreement with correctness: run the anomaly investigation in [audit and revision](references/audit-and-revision.md) for triggered results before calling the review complete. For a large independent review that is safely batchable, prefer resumable Codex CLI jobs with an exact model ID, read-only sandbox, strict output schema, non-overlapping packets, and bounded concurrency; persist each completed packet so a retry never repeats substantive review. Record the exact reviewed/total coverage, accepted unreviewed passes, reviewer, disagreements, adjudications, anomalies, and unresolved items. A review summary never replaces per-attempt evidence.
+8. Independent result review is opt-in: do not start it unless the user explicitly requests review. When requested without a broader scope, review machine failures only; review machine passes or every attempt only when the user explicitly asks for full review. Label a review performed with known machine outcomes as an evidence audit, separately from independent blind-review coverage; a fresh review-origin tag alone does not establish blindness. Keep the initial verdict blind, but do not equate agreement with correctness: run the anomaly investigation in [audit and revision](references/audit-and-revision.md) for triggered results before calling the review complete. For a large independent review that is safely batchable, prefer resumable Codex CLI jobs with an exact model ID, read-only sandbox, strict output schema, non-overlapping packets, and bounded concurrency; persist each completed packet so a retry never repeats substantive review. Record the exact reviewed/total coverage, accepted unreviewed passes, reviewer, disagreements, adjudications, anomalies, and unresolved items. A review summary never replaces per-attempt evidence.
9. Rerun only calls whose inputs, provider route/version, runtime contract, or source validity changed. Recompute downstream scores and reports without provider calls when only scoring, annotations, or presentation changed. Never retry merely to obtain a better result. A reference or verdict correction must identify the shared root cause and add executable positive/negative controls through the production scorer; bind those regression IDs in the revision receipt instead of patching stored verdicts.
10. Build the public report from the frozen evidence. Every page should answer a reader question. Show task and run identifiers, the exact expected-versus-actual evidence behind a verdict, denominators and unavailable metrics, method differences, limits, concise reproduction instructions, and a compact review history.
11. Release only after code tests, evidence integrity, report links, responsive/visual checks, secret scans, target-site checks, and deployed-output verification pass. Preserve earlier sites unless replacement was explicitly requested.
diff --git a/harness/web_fetch/report.py b/harness/web_fetch/report.py
index d4b5d0856..1460f7803 100644
--- a/harness/web_fetch/report.py
+++ b/harness/web_fetch/report.py
@@ -1084,9 +1084,10 @@ def review_summary(out, prefix='', *, highlight=True):
if not path.is_file():return ''
audit=json.loads(path.read_text())
if audit.get('retained_reviews'):
+ mode_note = (model_text(' 本次为已知程序结论的证据复核,不是独立盲审。', ' This round checked evidence with known machine outcomes; it was not an independent blind review.') if audit.get('review_protocol',{}).get('blind_initial_verdict') is False else '')
return ('
'+model_text(
f'当前复核覆盖 {audit.get("reviewed_attempts",0)}/{audit.get("expected_attempts",0)}:本次重新检查 {audit.get("fresh_reviewed_attempts",0)} 条,{audit.get("retained_reviewed_attempts",0)} 条沿用检查依据未变的旧复核。',
- f'Current review coverage: {audit.get("reviewed_attempts",0)}/{audit.get("expected_attempts",0)}: {audit.get("fresh_reviewed_attempts",0)} freshly reviewed; {audit.get("retained_reviewed_attempts",0)} prior reviews retained after verifying unchanged decision inputs.')+' '+link(prefix+'review/index.html','查看核验轮次 →')+'
')
+ f'Current review coverage: {audit.get("reviewed_attempts",0)}/{audit.get("expected_attempts",0)}: {audit.get("fresh_reviewed_attempts",0)} freshly reviewed; {audit.get("retained_reviewed_attempts",0)} prior reviews retained after verifying unchanged decision inputs.')+mode_note+' '+link(prefix+'review/index.html','查看核验轮次 →')+'')
if audit.get('adjudication_revision'):
count=len(audit['adjudication_revision']['adjudicated_attempt_ids'])
return ('
'+model_text(
@@ -2308,8 +2309,12 @@ def current_local_review_findings(out,dataset,records,audit):
if supplements and audit.get('retained_reviews'):
fresh = [row for row in audit.get('attempts', []) if row.get('review_origin') == 'fresh']
conflicts = sum(row.get('agent_verdict') != row.get('final_verdict') for row in fresh)
- rows.append((f'本次新复核先隐藏程序结论;初判与程序有 {conflicts} 条分歧,已对照原始返回和题目要求裁定。原始独立意见保留,未新增网页调用。',
- f'Fresh review withheld machine verdicts. {conflicts} initial disagreements were adjudicated against the original answers and task requirements. Initial independent opinions are preserved; no new webpage calls were added.'))
+ if audit.get('review_protocol',{}).get('blind_initial_verdict') is False:
+ rows.append(('本次复核时已知程序结论,逐条检查了保存的返回、任务要求和原生字段;不称为独立盲审,也未新增被测网页调用。旧复核的原始意见保留。',
+ 'This round inspected saved answers, task requirements and native fields with known machine outcomes. It was not an independent blind review and added no benchmark webpage calls. Original opinions from retained reviews are preserved.'))
+ else:
+ rows.append((f'本次新复核先隐藏程序结论;初判与程序有 {conflicts} 条分歧,已对照原始返回和题目要求裁定。原始独立意见保留,未新增网页调用。',
+ f'Fresh review withheld machine verdicts. {conflicts} initial disagreements were adjudicated against the original answers and task requirements. Initial independent opinions are preserved; no new webpage calls were added.'))
elif supplements:
supplement_rows = [json.loads(read_bound(out, item).read_text()) for item in supplements]
reviewed = sum(item['reviewed_attempts'] for item in supplement_rows)
@@ -2361,6 +2366,8 @@ def coverage(label,item):
'核验时隐藏机器结论和分数,只提供任务契约、标准答案与完整实际输出;分歧回到原始证据裁定,未决非零则禁止发布。任务和输出未变化时沿用原核验;发生变化的调用必须重新盲审。
')
if audit.get('adjudication_revision'):
method='
'+model_text('原独立判断和补充判断保留在逐条记录中。本次补充裁定查看了已有评分、契约和原始输出,不是新一轮全量盲审。每条裁定列出具体规则和原始证据片段;本地结果不因裁定完成而获得来源认证。','Original independent and supplemental opinions remain in the per-result records. This supplemental adjudication inspected saved scores, contracts and original answers; it is not a new full blind review. Each adjudication records specific rules and original excerpts. Completing adjudication does not certify source validity.')+'
'
+ if audit.get('review_protocol',{}).get('blind_initial_verdict') is False and audit.get('retained_reviews'):
+ method='
'+model_text('本次重新检查的记录采用已知程序结论的证据复核,逐条核对完整保存答案、任务要求和原生字段。其余记录经依赖检查沿用旧意见。两类范围分别统计;本次证据复核不算作新的独立盲审,也不补足历史来源认证。','The newly checked records received evidence review with known machine outcomes, checking complete saved answers, task requirements and native fields. Other records retain prior opinions after dependency checks. The two scopes are reported separately; this evidence review is not a new independent blind review and does not complete historical source certification.')+'
'
page+=section('核验口径',method)
dataset_path=out/'dataset.json'
if dataset_path.is_file():
diff --git a/tests/test_review_protocol_presentation.py b/tests/test_review_protocol_presentation.py
new file mode 100644
index 000000000..fb6572630
--- /dev/null
+++ b/tests/test_review_protocol_presentation.py
@@ -0,0 +1,49 @@
+"""A newly checked record must not be presented as a blind review by default."""
+import hashlib
+import json
+
+import pytest
+
+from harness.web_fetch import report
+
+
+@pytest.mark.parametrize('blind', [False, True])
+def test_current_review_text_reports_actual_blindness(tmp_path, blind):
+ replacement = {'provider_calls_added': 0, 'inherited_results': 1}
+ raw = json.dumps(replacement).encode()
+ (tmp_path/'replacement.json').write_bytes(raw)
+ dataset = {'cases': [{'id': 'case', 'track': 'web'}], 'comparison': {
+ 'task_replacement': {'path': 'replacement.json',
+ 'sha256': hashlib.sha256(raw).hexdigest()}}}
+ record = {'attempt_id': 'method__case', 'case_id': 'case', 'ok': True,
+ 'metrics': {'task_kind': 'body', 'content_pass': True}}
+ audit = {'kind': 'local_independent_review', 'review_protocol': {
+ 'scope': 'all', 'blind_initial_verdict': blind},
+ 'reviewer': {}, 'reviewed_attempts': 1, 'expected_attempts': 1,
+ 'fresh_reviewed_attempts': 1, 'retained_reviewed_attempts': 0,
+ 'retained_reviews': {'path': 'prior.json'},
+ 'supplemental_reviews': [{'path': 'supplement.json'}],
+ 'attempts': [{'attempt_id': record['attempt_id'], 'review_origin': 'fresh',
+ 'agent_verdict': 'pass', 'machine_verdict': 'pass',
+ 'final_verdict': 'pass'}],
+ 'review_rounds': [], 'unresolved': 0,
+ 'adjudication_revision': {'adjudicated_attempt_ids': []}}
+ (tmp_path/'result-review.json').write_text(json.dumps(audit))
+ (tmp_path/'dataset.json').write_text(json.dumps(dataset))
+ summary = report.review_summary(tmp_path)
+ findings = report.current_local_review_findings(tmp_path, dataset, [record], audit)
+ text = '\n'.join(row['text']+' '+row['text_en'] for row in findings)
+ if blind:
+ assert '先隐藏程序结论' in text
+ assert 'This round checked evidence with known machine outcomes' not in summary
+ else:
+ assert '已知程序结论' in summary
+ assert 'known machine outcomes' in summary
+ assert '先隐藏程序结论' not in text
+ assert 'Fresh review withheld machine verdicts' not in text
+ assert '不是独立盲审' in summary
+ report.write_review_page(tmp_path, [record])
+ page = (tmp_path/'review/index.html').read_text()
+ if not blind:
+ assert '本次重新检查的记录采用已知程序结论的证据复核' in page
+ assert '发生变化的调用必须重新盲审' not in page
From 1ca7e4505d7822fc28c3e8800ba246b1e2d77ad0 Mon Sep 17 00:00:00 2001
From: Maisiqi <324751762+Maisiqi@users.noreply.github.com>
Date: Fri, 16 Oct 2026 01:59:59 +0800
Subject: [PATCH 32/50] Freeze native route and request contracts before cohort
calls
---
.agents/skills/benchmark-report/SKILL.md | 2 +-
harness/web_fetch/README.md | 16 +
harness/web_fetch/artifacts.py | 4 +-
harness/web_fetch/native_routes.py | 385 ++++++++++++++++++
harness/web_fetch/providers/base.py | 29 ++
harness/web_fetch/providers/cloudflare.py | 8 +-
harness/web_fetch/providers/exa.py | 2 +-
harness/web_fetch/providers/firecrawl.py | 2 +-
harness/web_fetch/providers/moli.py | 8 +-
.../web_fetch/providers/subprocess_jsonl.py | 11 +
harness/web_fetch/providers/tavily.py | 2 +-
harness/web_fetch/reference_lifecycle.py | 4 +
harness/web_fetch/report_build.py | 6 +
harness/web_fetch/runner.py | 18 +
harness/web_fetch/workers/crawl4ai_worker.py | 3 +
tests/test_crawl4ai_file_worker.py | 12 +
tests/test_fixed_native_policy.py | 3 +-
tests/test_moli_provider.py | 19 +-
tests/test_native_route_contract.py | 361 ++++++++++++++++
tests/test_scoring.py | 1 +
tests/test_stable_task_lifecycle.py | 1 +
21 files changed, 876 insertions(+), 21 deletions(-)
create mode 100644 harness/web_fetch/native_routes.py
create mode 100644 tests/test_native_route_contract.py
diff --git a/.agents/skills/benchmark-report/SKILL.md b/.agents/skills/benchmark-report/SKILL.md
index 7d3b79403..c34c53439 100644
--- a/.agents/skills/benchmark-report/SKILL.md
+++ b/.agents/skills/benchmark-report/SKILL.md
@@ -19,7 +19,7 @@ Public report CLIs default to English where supported and expose `--language en|
1. Establish the decision being supported, the measured unit, cohort, versions, execution location, freshness policy, and intended deliverable. Inspect the actual dataset, runner, outputs, scorer, prior review evidence, and current report. Do not infer them from screenshots or prose.
2. Freeze a machine-readable evaluation contract before expensive calls. It must identify the exact cases and methods, run count, timeout, concurrency policy, input and output contract, scoring provenance, reference state, and publication target when one exists. For source-dependent tasks, fail closed: verify and freeze references immediately before calls, verify the same declared scope afterward, and certify the cohort only when both checks pass. Partial or uncertified cohorts remain diagnostic evidence and cannot enter a release.
-3. Let each method use its strongest documented route to produce the required output. Equalize the task, inputs, output contract, call budget, and scoring. Do not require identical internals. Record material asymmetries such as local versus remote execution, cold versus warm start, bundled conversion, network transit, account tier, and billable time.
+3. Let each method use its strongest documented route to produce the required output. Equalize the task, inputs, output contract, call budget, and scoring. Do not require identical internals. For fixed-native cohorts, declare the input media type independently of URL patterns and expected answers, freeze the class-level route and transport parameters before calls, and verify the retained request receipts at aggregation and release. Record material asymmetries such as local versus remote execution, cold versus warm start, bundled conversion, network transit, account tier, and billable time.
4. Preserve evidence as an append-only chain: task definition, reference, provider call, raw artifact, normalized artifact, score, review, revision, and release. Keep provider output and call identity immutable. A presentation or scoring change must not masquerade as a new provider result. Declare the native answer fields and validate their structural projection before accepting a scored artifact. Adapters must not remove returned candidates by URL meaning or consult expected answers when selecting candidate fields. Apply privacy redaction only to separately identified public views after scoring, preserving the private originals and distinguishing original, scored and public-view hashes.
5. Separate retrieval success, task acceptance, content quality, latency, cost, and freshness. A missing or partial reference makes the affected metric unavailable or scoped; it does not prove success or failure. Do not derive binary acceptance from an unrelated aggregate similarity score.
6. For heterogeneous same-window latency evidence, preserve the full predeclared task-by-method matrix. Show all-call latency on that common denominator separately from successful-call latency on each method's conditional subset. Treat the all-method shared-pass intersection as a diagnostic, never as a post-outcome selector or ranking sample.
diff --git a/harness/web_fetch/README.md b/harness/web_fetch/README.md
index ab7b0bd8a..c2a4299a4 100644
--- a/harness/web_fetch/README.md
+++ b/harness/web_fetch/README.md
@@ -608,6 +608,22 @@ risk-control family. Case-specific retrieval hints remain prohibited. Scoring
keeps the original case and frozen acceptance contract; family is not a reason
to inject scripts, convert HTML to Markdown, or fetch another URL.
+New fixed-native cohorts declare `source_benchmark.input_media_kind` for every
+case (`html_page`, `direct_pdf`, `direct_docx`, `direct_text`, `direct_image`, or
+`direct_media`). JSONL workers receive the same declaration as
+`input_media_kind`. Native document routes use this frozen type, not URL suffixes
+or expected answers. Historical calls without this declaration are not silently
+upgraded to the new protocol.
+
+Before any case call, the runner writes `native-route-contract.json`, binding
+every method/track/task-kind/input-type cell to its interface, answer fields,
+configuration and normalized transport parameters. Actual arguments are checked
+before transport; changing the task URL, endpoint, parameters or invocation count
+fails the run. Each result binds its request receipt to that pre-call contract.
+Aggregation retains the exact contract bytes; report and release checks verify
+its hash, complete declared scope and per-result receipts. A retrospective audit
+of old logs cannot substitute for this pre-call proof.
+
Resource tasks may select a declared native output: Firecrawl image/link/video
fields, Cloudflare's generic media-element scrape, and Crawl4AI media fields.
Moli uses native HTML for resource discovery and native Markdown for webpage
diff --git a/harness/web_fetch/artifacts.py b/harness/web_fetch/artifacts.py
index 3f3694d0d..3d35041de 100644
--- a/harness/web_fetch/artifacts.py
+++ b/harness/web_fetch/artifacts.py
@@ -357,7 +357,9 @@ def materialize_bound_artifacts(output: Path, source_root: Path, records: list[d
def materialize_reference_checks(output: Path, source: Path, batches: list[dict]) -> None:
- """Retain only hash-bound observations named by the execution certificates."""
+ """Retain hash-bound source observations and native execution contracts."""
+ from .native_routes import materialize_native_route_contracts
+ materialize_native_route_contracts(output, source, batches)
for batch in batches:
post = batch.get('reference_checks', {}).get('postcheck', {})
if not post:
diff --git a/harness/web_fetch/native_routes.py b/harness/web_fetch/native_routes.py
new file mode 100644
index 000000000..d6362ff5b
--- /dev/null
+++ b/harness/web_fetch/native_routes.py
@@ -0,0 +1,385 @@
+"""Declared native input and route contracts, independent of URLs and answers."""
+
+INPUT_MEDIA_KINDS = frozenset({
+ 'html_page', 'direct_pdf', 'direct_docx', 'direct_text',
+ 'direct_image', 'direct_media',
+})
+
+
+def declared_input_media_kind(case, *, required=True):
+ """Read a dataset declaration or its worker projection; never inspect a URL.
+
+ Dataset declarations live in ``source_benchmark.input_media_kind``. Workers
+ receive the same value as ``input_media_kind`` in the JSONL request.
+ """
+ get = case.get if isinstance(case, dict) else lambda key, default=None: getattr(case, key, default)
+ source = get('source_benchmark', {}) or {}
+ if not isinstance(source, dict):
+ raise ValueError('Native input media declaration must be an object')
+ nested, direct = source.get('input_media_kind'), get('input_media_kind')
+ if nested is not None and direct is not None and nested != direct:
+ raise ValueError('Conflicting native input media declarations')
+ value = direct if direct is not None else nested
+ if value is None and not required:
+ return None
+ if not isinstance(value, str) or value not in INPUT_MEDIA_KINDS:
+ raise ValueError('Native calls require a declared input_media_kind')
+ return value
+
+
+# Native parameter values come from the immutable provider configuration and its
+# pinned implementation, not a case URL, expected text, or provider outcome.
+def _digest(value):
+ import hashlib
+ import json
+ return hashlib.sha256(json.dumps(value, ensure_ascii=False, sort_keys=True,
+ separators=(',', ':')).encode()).hexdigest()
+
+
+def native_route_cell(provider, case):
+ media = declared_input_media_kind(case)
+ kind, track = case.task_kind, case.track
+ key = (provider.name, track, kind, media)
+ config, method = provider.config, provider.type
+ endpoint = None
+ if method == 'moli_webfetch':
+ transport = 'cli'
+ interface = ('raw_file' if kind == 'file' else 'native_resource_html'
+ if kind == 'resource' else 'native_document_bytes'
+ if track == 'document' and media in {'direct_pdf', 'direct_docx'}
+ else 'dynamic_native_markdown' if track == 'dynamic' else 'ordinary_markdown')
+ fields = ['original_bytes'] if interface in {'raw_file', 'native_document_bytes'} else ['stdout']
+ elif method in {'firecrawl', 'tavily_extract', 'exa_contents', 'cloudflare_markdown', 'cloudflare_content'}:
+ transport = 'http'
+ if method == 'firecrawl':
+ interface = '/v2/scrape'
+ endpoint = str(config.get('base_url', 'https://api.firecrawl.dev')).rstrip('/') + interface
+ fields = ['data.rawBase64'] if kind == 'file' else ['data.markdown', 'data.links', 'data.images', 'data.video']
+ elif method == 'tavily_extract':
+ interface = '/extract'
+ endpoint = str(config.get('base_url', 'https://api.tavily.com')).rstrip('/') + interface
+ fields = ['results[].raw_content', 'results[].images']
+ elif method == 'exa_contents':
+ interface = '/contents'
+ endpoint = str(config.get('base_url', 'https://api.exa.ai')).rstrip('/') + interface
+ fields = ['results[].text', 'results[].extras']
+ else:
+ interface = ('/scrape' if method == 'cloudflare_markdown' and kind == 'resource'
+ and track in {'image', 'video'} else '/content' if method == 'cloudflare_content'
+ else '/markdown')
+ endpoint = 'https://api.cloudflare.com/client/v4/accounts/' + str(config.get('account_id', '')) + '/browser-rendering' + interface
+ if method=='cloudflare_markdown' and interface=='/markdown' and config.get('browser'):
+ from urllib.parse import urlencode
+ endpoint += '?'+urlencode({'browser': config['browser']})
+ fields = ['result']
+ elif method in {'subprocess_jsonl', 'cowagent_builtin'}:
+ transport, interface = 'jsonl', 'native_worker_command'
+ fields = ['content', 'metadata.links', 'metadata.images', 'metadata.videos', 'files']
+ elif method == 'fixture':
+ transport, interface, fields = 'fixture', 'synthetic_fixture', ['content']
+ else:
+ raise ValueError('Unsupported fixed-native interface: ' + method)
+ options = ('max_bytes', 'max_age', 'max_age_hours', 'only_main_content', 'include_images',
+ 'extract_depth', 'text_verbosity', 'include_resource_links', 'rich_links_limit',
+ 'wait_until', 'block_private_networks', 'dns_mode', 'log_level', 'formats')
+ cell = {'method': key[0], 'provider_type': method, 'track': track, 'task_kind': kind, 'input_media_kind': media,
+ 'transport': transport, 'interface': interface, 'answer_fields': fields,
+ 'configured_parameters': {k: config[k] for k in options if k in config},
+ 'unspecified_parameters': 'native defaults from the bound implementation',
+ **({'command_sha256': _digest(config.get('command'))} if transport == 'jsonl' else {}),
+ 'provider_configuration_sha256': _digest(config),
+ 'endpoint_sha256': _digest(endpoint) if endpoint is not None else None}
+ return key, cell
+
+
+def _request_parameters(provider, case, cell, timeout_seconds):
+ """Explicit pre-call transport contract, separately checked against adapters."""
+ config, method = provider.config, provider.type
+ if cell['transport'] == 'http':
+ if method == 'tavily_extract':
+ body = {'urls': ['
'], 'extract_depth': config.get('extract_depth', 'advanced'),
+ 'format': 'markdown', 'include_images': bool(config.get('include_images', False))}
+ headers = {'Authorization': 'Bearer '+str(config.get('api_key', ''))}
+ elif method == 'exa_contents':
+ body = {'urls': [''], 'text': {'verbosity': config.get('text_verbosity', 'full')},
+ 'maxAgeHours': int(config.get('max_age_hours', 0))}
+ extras = {'links': 20, 'imageLinks': 20} if config.get('include_resource_links') else {}
+ if config.get('rich_links_limit'):
+ extras['richLinks'] = int(config.get('rich_links_limit', 100))
+ if extras:
+ body['extras'] = extras
+ headers = {'x-api-key': str(config.get('api_key', ''))}
+ elif method == 'firecrawl':
+ formats = ['rawBase64'] if case.task_kind == 'file' else list(config.get('formats', ['markdown']))
+ if case.track == 'video' and case.task_kind == 'resource' and 'video' not in formats:
+ formats.append('video')
+ body = {'url': '', 'formats': formats, 'maxAge': int(config.get('max_age', 0)),
+ 'proxy': str(config.get('proxy', 'auto'))}
+ if 'only_main_content' in config:
+ body['onlyMainContent'] = config['only_main_content']
+ if case.task_kind == 'file':
+ body['parsers'] = []
+ headers = ({'Authorization': 'Bearer '+str(config['api_key'])}
+ if config.get('api_key') else {})
+ else:
+ body = {'url': '', 'gotoOptions': {'waitUntil': config.get('wait_until', 'networkidle2')}}
+ if cell['interface'] == '/scrape':
+ selectors = (['img', 'picture source'] if case.track == 'image' else
+ ['video', 'video source', 'audio', 'audio source', 'iframe', 'a[href]'])
+ body['elements'] = [{'selector': v} for v in selectors]
+ headers = {'Authorization': 'Bearer '+str(config.get('api_token', ''))}
+ return {'method': 'POST', 'body': body, 'headers_sha256': _digest(headers),
+ 'timeout_seconds': timeout_seconds, 'max_bytes': int(config.get('max_bytes', 20_000_000))}
+ if cell['transport'] == 'cli':
+ argv = ['', 'fetch']
+ if cell['interface'] not in {'raw_file', 'native_document_bytes'}:
+ argv += ['--dump', 'html' if case.task_kind == 'resource' else 'markdown',
+ '--wait-until', config.get('wait_until', 'done')]
+ argv += ['--http-max-response-size', str(int(config.get('max_bytes', 20_000_000)))]
+ if config.get('block_private_networks', True):
+ argv.append('--block-private-networks')
+ if config.get('log_level'):
+ argv += ['--log-level', str(config['log_level'])]
+ argv.append('')
+ return {'argv': argv, 'binary_sha256': config.get('expected_binary_sha256')}
+ if cell['transport'] == 'jsonl':
+ request = {'url': '', 'log_level': config.get('log_level'),
+ 'diagnostic_path': '' if config.get('log_level')=='debug' else None,
+ 'timeout_seconds': timeout_seconds, 'uniform_native_parameters': True,
+ 'max_bytes': int(config.get('max_bytes', 20_000_000)),
+ 'dns_mode': config.get('dns_mode', 'system'), 'task_kind': case.task_kind,
+ 'track': case.track, 'input_media_kind': declared_input_media_kind(case),
+ 'retrieval_plan': None,
+ 'retrieval_mode': 'native_default' if case.track == 'dynamic' else 'declared_plan'}
+ command = [provider._resolve_token(part) for part in config.get('command', [])]
+ return {'request': request, 'command_sha256': _digest(command)}
+ return {}
+
+
+class NativeRouteLedger:
+ """Freeze class-level intent before calls; reject observed per-case drift.
+
+ Native defaults are bound by configuration and implementation identities.
+ Interfaces and normalized transport parameters are frozen for every cell
+ before the cohort. Actual arguments are checked before transport. No
+ response, URL pattern, or expected answer selects a route.
+ """
+ def __init__(self, cases, providers, *, timeout_seconds=45):
+ from datetime import datetime, timezone
+ import threading
+ self.cells, self.observed, self.calls, self.errors = {}, {}, {}, []
+ self.lock = threading.Lock()
+ self.providers = {p.name: p for p in providers if p.config.get('uniform_native_parameters')}
+ self.inventory = []
+ for case in cases:
+ media = declared_input_media_kind(case) if self.providers else None
+ if self.providers:
+ self.inventory.append({'case_id': case.id, 'track': case.track,
+ 'task_kind': case.task_kind, 'input_media_kind': media,
+ 'input_url_sha256': _digest(case.url)})
+ for provider in self.providers.values():
+ key, cell = native_route_cell(provider, case)
+ if case.track == 'dynamic':
+ from .retrieval_hints import native_default_case
+ if not native_default_case(case):
+ raise ValueError('Native dynamic inputs require a stable acceptance contract')
+ cell['request_parameters'] = _request_parameters(provider, case, cell, timeout_seconds)
+ if key in self.cells and self.cells[key] != cell:
+ raise ValueError('Native route intent differs within its declared class')
+ self.cells[key] = cell
+ self.document = {'schema_version': 1, 'kind': 'declared_native_route_contract',
+ 'frozen_at': datetime.now(timezone.utc).isoformat(),
+ 'pre_call_frozen': True,
+ 'parameter_policy': 'Interfaces, adapter request parameters and configured options frozen before the cohort; actual transport arguments must match. Unspecified product internals retain pinned native defaults.',
+ 'cases': self.inventory, 'cells': list(self.cells.values())}
+ self.binding = None
+
+ def write(self, source, *, dataset_sha256, runtime_source_sha256):
+ import hashlib
+ import json
+ self.document.update(dataset_sha256=dataset_sha256, runtime_source_sha256=runtime_source_sha256)
+ raw = (json.dumps(self.document, ensure_ascii=False, indent=2)+'\n').encode()
+ path = source/'native-route-contract.json'
+ if path.exists():
+ raise ValueError('Native route contract already exists; do not overwrite a run')
+ path.write_bytes(raw)
+ self.binding = {'path': path.name, 'sha256': hashlib.sha256(raw).hexdigest()}
+ for provider in self.providers.values():
+ provider._native_route_ledger = self
+ return self.binding
+
+ def reject(self, provider, case, reason):
+ with self.lock:
+ self.errors.append({'method': provider.name, 'case_id': case.id, 'reason': reason})
+ raise ValueError(reason)
+
+ def observe(self, provider, case, *, transport, parameters, endpoint=None, interface=None):
+ key, current = native_route_cell(provider, case)
+ cell = self.cells.get(key, {})
+ with self.lock:
+ message = 'Native route protocol was already violated' if self.errors else None
+ if {k:v for k,v in cell.items() if k!='request_parameters'} != current:
+ message = 'Native route configuration changed after the pre-call freeze'
+ elif transport != cell['transport']:
+ message = 'Native transport differs from its frozen route'
+ elif endpoint is not None and _digest(endpoint) != cell['endpoint_sha256']:
+ message = 'Native endpoint differs from its frozen route'
+ elif interface is not None and interface != cell['interface']:
+ message = 'Native command differs from its frozen route'
+ signature = {'transport': transport, 'interface': cell['interface'],
+ 'endpoint_sha256': cell['endpoint_sha256'], 'parameters': parameters}
+ digest = _digest(signature)
+ if not message and parameters != cell.get('request_parameters'):
+ message = 'Native request parameters differ from the pre-call contract'
+ if not message and key in self.observed and self.observed[key] != digest:
+ message = 'Native request signature changed within the same declared class'
+ identity = (provider.name, case.id)
+ if not message and self.calls.get(identity):
+ message = 'Fixed-native calls permit only one adapter invocation per task'
+ if message:
+ self.errors.append({'method': provider.name, 'case_id': case.id, 'reason': message})
+ raise ValueError(message)
+ self.observed[key] = digest
+ self.calls.setdefault(identity, []).append({'signature': signature, 'sha256': digest})
+
+ def receipt(self, provider, case):
+ from copy import deepcopy
+ key, _ = native_route_cell(provider, case)
+ with self.lock:
+ return {'contract_sha256': self.binding['sha256'], 'cell': list(key),
+ 'requests': deepcopy(self.calls.get((provider.name, case.id), [])),
+ 'protocol_errors': [dict(e) for e in self.errors if e['method']==provider.name and e['case_id']==case.id]}
+
+
+def verify_native_route_contract(source, manifest, dataset, records):
+ """Shared execution/release check for newly declared native cohorts only."""
+ import hashlib
+ import json
+ from datetime import datetime
+ from .safety import contained_path
+ if manifest.get('native_route_errors'):
+ raise ValueError('Native route protocol was violated')
+ binding = manifest.get('native_route_contract')
+ if not binding:
+ if manifest.get('native_routes_required'):
+ raise ValueError('Fixed-native cohort lacks its pre-call route contract')
+ return None
+ path = contained_path(source, binding['path'], reject_symlinks=True)
+ raw = path.read_bytes()
+ if hashlib.sha256(raw).hexdigest() != binding.get('sha256'):
+ raise ValueError('Native route contract hash changed')
+ doc = json.loads(raw)
+ if (doc.get('kind') != 'declared_native_route_contract' or doc.get('pre_call_frozen') is not True
+ or doc.get('dataset_sha256') != manifest.get('dataset_sha256')
+ or doc.get('runtime_source_sha256') != manifest.get('runtime_source_sha256')):
+ raise ValueError('Native route contract differs from the frozen execution')
+ cells = {(c['method'], c['track'], c['task_kind'], c['input_media_kind']): c for c in doc['cells']}
+ cases = {c['id']: c for c in dataset['cases']}
+ inventory = {c['case_id']: c for c in doc['cases']}
+ if len(cells) != len(doc['cells']) or len(inventory) != len(doc['cases']):
+ raise ValueError('Native route contract has duplicate declarations')
+ names = {key[0] for key in cells}
+ required = {p['name'] for p in manifest.get('providers', []) if p.get('fixed_native_parameters')}
+ projection = manifest.get('projection', {}).get('kind')
+ allowed_projection = projection in {'complete-provider-case-subset-from-retained-run',
+ 'complete-provider-from-retained-incomplete-matrix', 'completed-case-from-live-preflight'}
+ if (not required or not required <= names or (required != names and not allowed_projection)):
+ raise ValueError('Native route method scope differs from the frozen execution')
+ names = required
+ selected_cases = ({row['case_id'] for row in records if row['provider'] in names}
+ if allowed_projection else set(inventory))
+ if not selected_cases <= set(inventory):
+ raise ValueError('Native route projection changes its frozen input scope')
+ expected = {(name, identifier) for name in names for identifier in selected_cases}
+ actual, signatures = set(), {}
+ for row in records:
+ if row['provider'] not in names:
+ continue
+ identity = row['provider'], row['case_id']
+ if identity in actual or identity not in expected:
+ raise ValueError('Native route result inventory differs')
+ actual.add(identity)
+ case = cases[row['case_id']]
+ media = declared_input_media_kind(case)
+ if (_digest(case['url']) != inventory[row['case_id']]['input_url_sha256']
+ or inventory[row['case_id']] != {'case_id': row['case_id'], 'track': case.get('track', 'web'),
+ 'task_kind': case.get('task_kind', 'body'), 'input_media_kind': media,
+ 'input_url_sha256': _digest(row['url'])}
+ or datetime.fromisoformat(doc['frozen_at']) > datetime.fromisoformat(row['started_at'])):
+ raise ValueError('Native input type or pre-call freeze differs')
+ key = row['provider'], case.get('track', 'web'), case.get('task_kind', 'body'), media
+ receipt = row.get('provider_metadata', {}).get('native_route_receipt', {})
+ if receipt.get('protocol_errors'):
+ raise ValueError('Native request violated its route contract')
+ if receipt.get('contract_sha256') != binding['sha256'] or receipt.get('cell') != list(key) or key not in cells:
+ raise ValueError('Native result lacks its bound route receipt')
+ requests = receipt.get('requests', [])
+ if len(requests) > 1 or (row['ok'] and not requests and cells[key]['transport'] != 'fixture'):
+ raise ValueError('Native result lacks one complete invocation signature')
+ for item in requests:
+ signature = item['signature']
+ if (_digest(signature) != item.get('sha256')
+ or signature['transport'] != cells[key]['transport']
+ or signature['interface'] != cells[key]['interface']
+ or signature['endpoint_sha256'] != cells[key]['endpoint_sha256']
+ or signature.get('parameters') != cells[key]['request_parameters']):
+ raise ValueError('Native invocation differs from its frozen interface')
+ if key in signatures and signatures[key] != item['sha256']:
+ raise ValueError('Native request signature differs within its declared class')
+ signatures[key] = item['sha256']
+ if actual != expected:
+ raise ValueError('Native route contract has an incomplete result scope')
+ return doc
+
+
+def verify_native_execution_manifests(source, manifests, dataset, records):
+ """Check native contracts at every existing aggregate/report boundary."""
+ checked = []
+ for manifest in manifests:
+ if not manifest.get('native_routes_required') and not manifest.get('native_route_contract'):
+ continue
+ if source is None or dataset is None or records is None:
+ raise ValueError('Native route verification requires retained inputs and complete records')
+ identifiers = manifest.get('attempt_ids', [])
+ if len(identifiers) != len(set(identifiers)):
+ raise ValueError('Native execution repeats attempt identities')
+ identifier_set = set(identifiers)
+ selected = [row for row in records if row['attempt_id'] in identifier_set]
+ if {r['attempt_id'] for r in selected} != set(identifiers):
+ raise ValueError('Native execution omits retained attempt evidence')
+ checked.append(verify_native_route_contract(source, manifest, dataset, selected))
+ return checked
+
+
+def materialize_native_route_contracts(output, source, manifests):
+ """Copy hash-bound frozen contracts, deduplicating multiple cohort roots."""
+ import hashlib
+ import re
+ import shutil
+ from .safety import contained_path
+ for manifest in manifests:
+ binding = manifest.get('native_route_contract')
+ if not binding:
+ if manifest.get('native_routes_required'):
+ raise ValueError('Native cohort is missing its frozen route contract')
+ continue
+ digest = binding.get('sha256', '')
+ if not isinstance(digest, str) or re.fullmatch('[0-9a-f]{64}', digest) is None:
+ raise ValueError('Invalid native route contract digest')
+ relative = binding.get('path')
+ if relative not in {'native-route-contract.json', 'native-route-contracts/'+digest+'.json'}:
+ raise ValueError('Invalid native route contract path')
+ path = contained_path(source, relative, reject_symlinks=True)
+ if not path.is_file() or hashlib.sha256(path.read_bytes()).hexdigest() != digest:
+ raise ValueError('Native route contract bytes changed')
+ target_relative = 'native-route-contracts/'+digest+'.json'
+ target = contained_path(output, target_relative, reject_symlinks=True)
+ target.parent.mkdir(parents=True, exist_ok=True)
+ if target.exists():
+ if not target.is_file() or hashlib.sha256(target.read_bytes()).hexdigest() != digest:
+ raise ValueError('Conflicting native route contract bytes')
+ else:
+ shutil.copy2(path, target)
+ if hashlib.sha256(target.read_bytes()).hexdigest() != digest:
+ raise ValueError('Copied native route contract bytes changed')
+ manifest['native_route_contract'] = {'path': target_relative, 'sha256': digest}
diff --git a/harness/web_fetch/providers/base.py b/harness/web_fetch/providers/base.py
index fe443e3a8..3e0f525a0 100644
--- a/harness/web_fetch/providers/base.py
+++ b/harness/web_fetch/providers/base.py
@@ -21,6 +21,35 @@ def check_native_call(self, case: EvalCase) -> None:
if case.task_kind not in {"body", "structure", "resource", "file"}:
raise ValueError("fixed native calls require a declared native output kind")
+ def capture_native_request(self, case, *, transport, parameters, endpoint=None, interface=None):
+ ledger = getattr(self, '_native_route_ledger', None)
+ if ledger is not None:
+ ledger.observe(self, case, transport=transport, parameters=parameters,
+ endpoint=endpoint, interface=interface)
+
+ def native_http_request(self, case, callback, endpoint, **kwargs):
+ """Observe exact transport arguments before a fixed-native HTTP call."""
+ ledger = getattr(self, '_native_route_ledger', None)
+ if ledger is not None:
+ from copy import deepcopy
+ from ..native_routes import _digest
+ body = deepcopy(kwargs.get('json_body'))
+ if not isinstance(body, dict):
+ ledger.reject(self, case, 'Native HTTP request lacks its declared input field')
+ slots = [key for key in ('url', 'urls') if key in body]
+ if slots == ['url'] and body['url'] == case.url:
+ body['url'] = ''
+ elif slots == ['urls'] and body['urls'] == [case.url]:
+ body['urls'] = ['']
+ else:
+ ledger.reject(self, case, 'Native HTTP input differs from the frozen task URL')
+ self.capture_native_request(case, transport='http', endpoint=endpoint,
+ parameters={'method': kwargs.get('method', 'GET'), 'body': body,
+ 'headers_sha256': _digest(kwargs.get('headers', {})),
+ 'timeout_seconds': kwargs.get('timeout'),
+ 'max_bytes': kwargs.get('max_bytes')})
+ return callback(endpoint, **kwargs)
+
@property
def min_interval_seconds(self) -> float:
"""Minimum cooldown from one response completion to the next request start."""
diff --git a/harness/web_fetch/providers/cloudflare.py b/harness/web_fetch/providers/cloudflare.py
index 01b2739b8..7ba21b37a 100644
--- a/harness/web_fetch/providers/cloudflare.py
+++ b/harness/web_fetch/providers/cloudflare.py
@@ -250,7 +250,7 @@ def _fetch_payload(self, case: EvalCase, timeout: float, request_body: dict) ->
account_id=account_id,
http_timeout=http_timeout,
)
- if _uses_direct_document_route(case) and not self.config.get("uniform_native_parameters"):
+ if not self.config.get("uniform_native_parameters") and _uses_direct_document_route(case):
return self._fetch_direct_document(
case,
account_id=account_id,
@@ -346,7 +346,7 @@ def _fetch_payload(self, case: EvalCase, timeout: float, request_body: dict) ->
"client_http_timeout_ms": int(http_timeout * 1000),
"wait_until": wait_until,
}
- response = request(
+ response = self.native_http_request(case, request,
endpoint + ("?" + query if query else ""),
method="POST",
timeout=http_timeout,
@@ -431,7 +431,7 @@ def _fetch_media_resources(
# One native media request for every video-resource task; never
# choose selectors from expected URLs or reference answers.
selectors = ["video", "video source", "audio", "audio source", "iframe", "a[href]"]
- response = request(
+ response = self.native_http_request(case, request,
f"https://api.cloudflare.com/client/v4/accounts/{account_id}/browser-rendering/scrape",
method="POST",
timeout=http_timeout,
@@ -686,7 +686,7 @@ def fetch(self, case: EvalCase, timeout: float) -> ProviderResult:
account_id = str(self.config.get("account_id", ""))
http_timeout = inner_timeout_budget(timeout, native_default=bool(self.config.get("uniform_native_parameters")))
service_timeout_ms = min(max(1, int(http_timeout * 1000)), 60_000)
- response = request(
+ response = self.native_http_request(case, request,
f"https://api.cloudflare.com/client/v4/accounts/{account_id}/browser-rendering/content",
method="POST",
timeout=http_timeout,
diff --git a/harness/web_fetch/providers/exa.py b/harness/web_fetch/providers/exa.py
index 393956e51..61a3a9bec 100644
--- a/harness/web_fetch/providers/exa.py
+++ b/harness/web_fetch/providers/exa.py
@@ -72,7 +72,7 @@ def fetch(self, case: EvalCase, timeout: float) -> ProviderResult:
# Exa's richLinks route returns the explicit anchor/URL pair needed
# by the shared named-link answer contract.
extras["richLinks"] = int(self.config.get("rich_links_limit", 100))
- response = request(
+ response = self.native_http_request(case, request,
f"{str(self.config.get('base_url', 'https://api.exa.ai')).rstrip('/')}/contents",
method="POST",
timeout=http_timeout,
diff --git a/harness/web_fetch/providers/firecrawl.py b/harness/web_fetch/providers/firecrawl.py
index 543f68735..2949080a2 100644
--- a/harness/web_fetch/providers/firecrawl.py
+++ b/harness/web_fetch/providers/firecrawl.py
@@ -287,7 +287,7 @@ def fetch(self, case: EvalCase, timeout: float) -> ProviderResult:
"Firecrawl dynamic_wait_strategy must be actions or wait_for"
),
)
- response = request(
+ response = self.native_http_request(case, request,
f"{base_url}/v2/scrape",
method="POST",
timeout=http_timeout,
diff --git a/harness/web_fetch/providers/moli.py b/harness/web_fetch/providers/moli.py
index 3d5425107..d2b799aca 100644
--- a/harness/web_fetch/providers/moli.py
+++ b/harness/web_fetch/providers/moli.py
@@ -357,8 +357,9 @@ def _route_for_case(case: EvalCase, *, native_default: bool = False) -> str:
if native_default:
if case.task_kind == "resource":
return "native_resource_html"
- path = urlsplit(case.url).path.casefold()
- if case.track == "document" and (path.endswith((".pdf", ".docx")) or "/pdf/" in path):
+ from ..native_routes import declared_input_media_kind
+ media = declared_input_media_kind(case, required=case.track == "document")
+ if case.track == "document" and media in {"direct_pdf", "direct_docx"}:
return "native_document_bytes"
return "dynamic_native_markdown" if case.track == "dynamic" else "ordinary_markdown"
if case.task_kind == "resource":
@@ -794,6 +795,9 @@ def fetch(self, case: EvalCase, timeout: float) -> ProviderResult:
error_message=f"Cannot read Moli binary: {error}",
metadata=metadata,
)
+ self.capture_native_request(case, transport='cli', interface=route,
+ parameters={'argv': ['', *command[1:-1], ''],
+ 'binary_sha256': getattr(self, '_binary_sha256', None)})
# Spool artifacts to files rather than retaining arbitrary CLI output in RAM.
with ExitStack() as dns_stack, ExitStack() as spool_stack:
sink = getattr(self, "_call_diagnostics", None)
diff --git a/harness/web_fetch/providers/subprocess_jsonl.py b/harness/web_fetch/providers/subprocess_jsonl.py
index 6ab6409cc..2888c6bb3 100644
--- a/harness/web_fetch/providers/subprocess_jsonl.py
+++ b/harness/web_fetch/providers/subprocess_jsonl.py
@@ -195,6 +195,9 @@ def _request_stderr(self, cursor: tuple[int, int]) -> dict[str, Any]:
def fetch(self, case: EvalCase, timeout: float) -> ProviderResult:
self.check_native_call(case)
+ from ..native_routes import declared_input_media_kind
+ input_media = declared_input_media_kind(case, required=bool(
+ self.config.get("uniform_native_parameters") and case.track == "document"))
answer_begin = time.perf_counter()
try:
retrieval_plan = plan_for_case(case)
@@ -225,9 +228,17 @@ def fetch(self, case: EvalCase, timeout: float) -> ProviderResult:
"dns_mode": self.config.get("dns_mode", "system"),
"task_kind": case.task_kind,
"track": case.track,
+ **({"input_media_kind": input_media} if input_media is not None else {}),
"retrieval_plan": retrieval_plan.worker_dict() if retrieval_plan else None,
"retrieval_mode": "native_default" if native_default_case(case) else "declared_plan",
}
+ if getattr(self, '_native_route_ledger', None) is not None:
+ from ..native_routes import _digest
+ projected = {**request, 'url': '',
+ 'diagnostic_path': '' if request['diagnostic_path'] else None}
+ self.capture_native_request(case, transport='jsonl', interface='native_worker_command',
+ parameters={'request': projected,
+ 'command_sha256': _digest([self._resolve_token(part) for part in self.config['command']])})
if self._active_diagnostics: self._active_diagnostics.emit("caller", "worker_request_sent", request=request)
try:
process.stdin.write(json.dumps(request, ensure_ascii=False) + "\n")
diff --git a/harness/web_fetch/providers/tavily.py b/harness/web_fetch/providers/tavily.py
index b826d15b3..7b8c13893 100644
--- a/harness/web_fetch/providers/tavily.py
+++ b/harness/web_fetch/providers/tavily.py
@@ -48,7 +48,7 @@ def fetch(self, case: EvalCase, timeout: float) -> ProviderResult:
# deadline remains authoritative and we omit an invalid API value.
if extract_timeout >= 1.0 and not self.config.get("uniform_native_parameters"):
request_body["timeout"] = round(extract_timeout, 3)
- response = request(
+ response = self.native_http_request(case, request,
f"{str(self.config.get('base_url', 'https://api.tavily.com')).rstrip('/')}/extract",
method="POST",
timeout=http_timeout,
diff --git a/harness/web_fetch/reference_lifecycle.py b/harness/web_fetch/reference_lifecycle.py
index efd9ca9b6..556f3369d 100644
--- a/harness/web_fetch/reference_lifecycle.py
+++ b/harness/web_fetch/reference_lifecycle.py
@@ -1356,6 +1356,10 @@ def current_protocol(batch):
def valid_hash(value):
return isinstance(value, str) and re.fullmatch(r"[0-9a-f]{64}", value) is not None
+ from .native_routes import verify_native_execution_manifests
+ verify_native_execution_manifests(source, manifests,
+ {'cases': [dict(c) if isinstance(c, dict) else vars(c) for c in case_map.values()]}
+ if case_map is not None else None, records)
allow_scoring_rule_revision = (allow_scoring_rule_revision or valid_scoring_revision(source))
revised_references = reference_revision_cases(source)
original_latency = latency_correction_original_elapsed(source, records=records)
diff --git a/harness/web_fetch/report_build.py b/harness/web_fetch/report_build.py
index 258a78ba5..d6c30db3a 100644
--- a/harness/web_fetch/report_build.py
+++ b/harness/web_fetch/report_build.py
@@ -411,6 +411,8 @@ def build_report(source: Path, output: Path, *, include_english: bool = False,
raise ValueError('A report requires tasks and measured records')
execution = source/'execution.json'
manifests = json.loads(execution.read_text())['batches'] if execution.exists() else [json.loads((source/'manifest.json').read_text())]
+ from .native_routes import verify_native_execution_manifests
+ verify_native_execution_manifests(source, manifests, dataset, records)
if not local_audit and not diagnostic_preview:
require_verified_reference_checks(manifests, cases=dataset, records=records, source=source,
equivalent_capture_bindings=dataset.get('comparison',{}).get('reference_binding_mode') ==
@@ -1304,6 +1306,10 @@ def validate_local_presentation(output: Path, binding_path: Path):
require_host_metadata_projection(output, binding['publication_projection'])
dataset = json.loads((output / 'dataset.json').read_text())
records = load_jsonl(contained_path(output, 'results.jsonl'))
+ from .native_routes import verify_native_execution_manifests
+ execution = output/'execution.json'
+ if execution.is_file():
+ verify_native_execution_manifests(output, json.loads(execution.read_text())['batches'], dataset, records)
scoring_sources = {row['attempt_id']: binding.get('measured_scoring_source_sha256') for row in records}
if binding.get('offline_revision'):
from .local_revision import validate_revision_chain, read_bound
diff --git a/harness/web_fetch/runner.py b/harness/web_fetch/runner.py
index 710c217f4..65be6fe70 100644
--- a/harness/web_fetch/runner.py
+++ b/harness/web_fetch/runner.py
@@ -495,8 +495,10 @@ def run_evaluation(
if provider.config.get("uniform_native_parameters"):
if runs != 1 or max_failure_retries != 0:
raise ValueError("fixed native calls require one run and zero caller retries")
+ from .native_routes import declared_input_media_kind
for case in cases:
provider.check_native_call(case)
+ declared_input_media_kind(case)
safe_identifier(provider.name)
for case in cases:
validate_case(case)
@@ -504,6 +506,9 @@ def run_evaluation(
raise ValueError('Provider names must be unique within an execution')
if len({case.id for case in cases}) != len(cases):
raise ValueError('Task IDs must be unique within an execution')
+ from .native_routes import NativeRouteLedger
+ native_routes = (NativeRouteLedger(cases, providers, timeout_seconds=timeout)
+ if any(p.config.get('uniform_native_parameters') for p in providers) else None)
for filename in ['artifacts', PUBLIC_CALL_LOG_ROOT, 'manifest.json', 'dataset.json',
'results.jsonl', 'summary.json']:
contained_path(output_dir, filename)
@@ -562,6 +567,7 @@ def run_evaluation(
"name": provider.name,
"type": provider.type,
"min_interval_seconds": provider.min_interval_seconds,
+ "fixed_native_parameters": bool(provider.config.get("uniform_native_parameters")),
**({"implementation_identity": provider._implementation_identity}
if isinstance(getattr(provider, '_implementation_identity', None), dict) else {}),
}
@@ -596,6 +602,11 @@ def run_evaluation(
manifest['evidence_state'] = 'diagnostic'
manifest['unqualified_reference_cases'] = sorted(
set(comparison.get('unqualified_reference_cases', [])) & {case.id for case in cases})
+ if native_routes is not None:
+ manifest['native_routes_required'] = True
+ manifest['native_route_contract'] = native_routes.write(output_dir,
+ dataset_sha256=manifest['dataset_sha256'],
+ runtime_source_sha256=manifest['runtime_source_sha256'])
(output_dir / "manifest.json").write_text(
json.dumps(manifest, ensure_ascii=False, indent=2) + "\n", encoding="utf-8"
)
@@ -694,6 +705,8 @@ def run_provider(provider: Provider) -> None:
suffix = (f'run-{run_number:03d}.txt' if max_failure_retries == 0 else
f'run-{run_number:03d}-physical-{physical_attempt:03d}.txt')
artifact_rel = Path('artifacts') / provider.name / case.id / suffix
+ if native_routes is not None and provider.name in native_routes.providers:
+ result.metadata['native_route_receipt'] = native_routes.receipt(provider, case)
artifact = persist_result_artifacts(output_dir, artifact_rel, result)
artifact_bytes = result.content.encode('utf-8', errors='replace')
measured = score_attempt(
@@ -858,12 +871,17 @@ def run_provider(provider: Provider) -> None:
manifest["completed_at"] = utc_now()
manifest["attempt_count"] = len(records)
manifest["attempt_ids"] = [r['attempt_id'] for r in records]
+ if native_routes is not None:
+ manifest['native_route_errors'] = list(native_routes.errors)
(output_dir / "manifest.json").write_text(
json.dumps(manifest, ensure_ascii=False, indent=2) + "\n", encoding="utf-8"
)
(output_dir / "summary.json").write_text(
json.dumps(summary, ensure_ascii=False, indent=2) + "\n", encoding="utf-8"
)
+ if native_routes is not None:
+ from .native_routes import verify_native_route_contract
+ verify_native_route_contract(output_dir, manifest, dataset_meta, records)
expected_attempts = len(cases) * len(providers) * runs
if len(records) != expected_attempts or manifest["unexecuted"]:
raise IncompleteEvaluationError(
diff --git a/harness/web_fetch/workers/crawl4ai_worker.py b/harness/web_fetch/workers/crawl4ai_worker.py
index 5887b7485..043b46e3e 100755
--- a/harness/web_fetch/workers/crawl4ai_worker.py
+++ b/harness/web_fetch/workers/crawl4ai_worker.py
@@ -145,6 +145,9 @@ def is_direct_pdf_document(request):
return False
if request.get("task_kind", "body") != "body":
return False
+ if request.get("uniform_native_parameters"):
+ from harness.web_fetch.native_routes import declared_input_media_kind
+ return declared_input_media_kind(request) == "direct_pdf"
path = urlsplit(request["url"]).path.lower().rstrip("/")
return path.endswith(".pdf") or "/pdf/" in path
diff --git a/tests/test_crawl4ai_file_worker.py b/tests/test_crawl4ai_file_worker.py
index 69ecc25e0..86f2da606 100644
--- a/tests/test_crawl4ai_file_worker.py
+++ b/tests/test_crawl4ai_file_worker.py
@@ -162,6 +162,18 @@ def test_direct_pdf_selection_is_limited_to_document_body_tasks(self):
):
self.assertFalse(worker.is_direct_pdf_document(request))
+ def test_fixed_native_pdf_route_uses_declared_input_not_url_shape(self):
+ worker = self.load_worker(object)
+ request = {'url': 'https://example.test/no-extension', 'track': 'document',
+ 'task_kind': 'body', 'uniform_native_parameters': True,
+ 'input_media_kind': 'direct_pdf'}
+ self.assertTrue(worker.is_direct_pdf_document(request))
+ request.update(url='https://example.test/looks-like.pdf', input_media_kind='html_page')
+ self.assertFalse(worker.is_direct_pdf_document(request))
+ request.pop('input_media_kind')
+ with self.assertRaisesRegex(ValueError, 'declared input_media_kind'):
+ worker.is_direct_pdf_document(request)
+
def test_direct_pdf_uses_native_pdf_strategies_and_returns_markdown(self):
calls = []
pdf_crawler_instance = object()
diff --git a/tests/test_fixed_native_policy.py b/tests/test_fixed_native_policy.py
index 860a53f96..973d72e5c 100644
--- a/tests/test_fixed_native_policy.py
+++ b/tests/test_fixed_native_policy.py
@@ -85,6 +85,7 @@ def test_native_cloudflare_document_never_downloads_in_caller():
provider = build_provider("cf", {"type": "cloudflare_markdown", "uniform_native_parameters": True})
case = EvalCase("pdf", "pdf", "https://example.com/a.pdf", "control", [], track="document")
with patch("harness.web_fetch.providers.cloudflare._get_document", side_effect=AssertionError("extra download")), \
+ patch("harness.web_fetch.providers.cloudflare._uses_direct_document_route", side_effect=AssertionError("native URL guessing")), \
patch("harness.web_fetch.providers.cloudflare.request") as call:
call.return_value = HttpResponse(200, case.url, {}, b'{"success":true,"result":"native text"}')
assert provider.fetch(case, 45).content == "native text"
@@ -109,7 +110,7 @@ def test_native_cloudflare_media_has_no_answer_dependent_selectors_or_timers():
assert "timeout" not in bodies[0]["gotoOptions"]
-def test_all_430_frozen_cases_pass_native_input_guard():
+def test_historical_430_cases_pass_native_hint_and_output_guard():
from harness.web_fetch.config import load_dataset
provider = build_provider("fixture", {"type": "fixture", "uniform_native_parameters": True})
count = 0
diff --git a/tests/test_moli_provider.py b/tests/test_moli_provider.py
index d1f16ee85..a6c7dae46 100644
--- a/tests/test_moli_provider.py
+++ b/tests/test_moli_provider.py
@@ -211,15 +211,16 @@ def test_native_families_never_use_caller_scripts_or_conversion(self):
" assert sys.argv[sys.argv.index('--dump') + 1] in ('markdown', 'html')\n"
"print('native output')\n", uniform_native_parameters=True)
with patch("harness.web_fetch.providers.moli.extract_document_text", side_effect=AssertionError("caller conversion")):
- for track, kind, url in [("challenge", "structure", "https://example.com/"),
- ("risk_control", "body", "https://example.com/"),
- ("document", "body", "https://example.com/a.docx"),
- ("document", "body", "https://example.com/a.pdf"),
- ("document", "body", "https://example.com/viewer.html"),
- ("image", "resource", "https://example.com/"),
- ("video", "resource", "https://example.com/"),
- ("video", "body", "https://example.com/")]:
- result = provider.fetch(self.case_with(track=track, task_kind=kind, url=url), 5)
+ for track, kind, url, media in [("challenge", "structure", "https://example.com/", "html_page"),
+ ("risk_control", "body", "https://example.com/", "html_page"),
+ ("document", "body", "https://example.com/a.docx", "direct_docx"),
+ ("document", "body", "https://example.com/a.pdf", "direct_pdf"),
+ ("document", "body", "https://example.com/viewer.html", "html_page"),
+ ("image", "resource", "https://example.com/", "html_page"),
+ ("video", "resource", "https://example.com/", "html_page"),
+ ("video", "body", "https://example.com/", "html_page")]:
+ result = provider.fetch(self.case_with(track=track, task_kind=kind, url=url,
+ source_benchmark={"input_media_kind": media}), 5)
self.assertTrue(result.ok, result.error_message)
if url.endswith((".pdf", ".docx")):
self.assertEqual(result.content, "")
diff --git a/tests/test_native_route_contract.py b/tests/test_native_route_contract.py
new file mode 100644
index 000000000..a19e5fe34
--- /dev/null
+++ b/tests/test_native_route_contract.py
@@ -0,0 +1,361 @@
+"""Declared input types select native routes without URL or answer inspection."""
+from types import SimpleNamespace
+
+import pytest
+
+from harness.web_fetch.native_routes import INPUT_MEDIA_KINDS, declared_input_media_kind
+from harness.web_fetch.providers.moli import _route_for_case
+
+
+@pytest.mark.parametrize('media', sorted(INPUT_MEDIA_KINDS))
+def test_dataset_and_worker_use_the_same_explicit_input_type(media):
+ assert declared_input_media_kind({'source_benchmark': {'input_media_kind': media}}) == media
+ assert declared_input_media_kind({'input_media_kind': media}) == media
+
+
+@pytest.mark.parametrize('value', [None, '', 'guess_from_url', [], {}])
+def test_invalid_or_missing_input_type_is_not_guessed(value):
+ with pytest.raises(ValueError, match='declared input_media_kind'):
+ declared_input_media_kind({'url': 'https://example.test/actual.pdf',
+ 'input_media_kind': value})
+
+
+def test_conflicting_input_declarations_are_rejected():
+ with pytest.raises(ValueError, match='Conflicting'):
+ declared_input_media_kind({'input_media_kind': 'html_page',
+ 'source_benchmark': {'input_media_kind': 'direct_pdf'}})
+
+
+@pytest.mark.parametrize('media,expected', [
+ ('direct_pdf', 'native_document_bytes'), ('direct_docx', 'native_document_bytes'),
+ ('html_page', 'ordinary_markdown'), ('direct_text', 'ordinary_markdown'),
+])
+def test_moli_document_selection_cannot_read_url_or_reference(media, expected):
+ class DeclaredDocument:
+ track = 'document'
+ task_kind = 'body'
+ source_benchmark = {'input_media_kind': media}
+ @property
+ def url(self):
+ raise AssertionError('route selection inspected the URL')
+ @property
+ def expected_resources(self):
+ raise AssertionError('route selection inspected expected answers')
+ assert _route_for_case(DeclaredDocument(), native_default=True) == expected
+
+
+def test_fixed_native_cohort_rejects_undeclared_media_before_preflight(tmp_path):
+ from harness.web_fetch.models import EvalCase
+ from harness.web_fetch.providers.base import build_provider
+ from harness.web_fetch.runner import run_evaluation
+ provider = build_provider('fixture', {'type': 'fixture', 'uniform_native_parameters': True})
+ case = EvalCase('case', 'Case', 'https://example.test/a.pdf', 'control', [])
+ with pytest.raises(ValueError, match='declared input_media_kind'):
+ run_evaluation(output_dir=tmp_path/'run', dataset_meta={}, dataset_path=tmp_path/'absent.json',
+ cases=[case], providers=[provider], provider_config_path=tmp_path/'absent-providers.json',
+ runs=1, timeout=45, delay=0)
+ assert not (tmp_path/'run').exists()
+
+
+def test_plain_provider_use_does_not_infer_optional_media():
+ assert declared_input_media_kind(SimpleNamespace(source_benchmark={}), required=False) is None
+
+
+def _route_http_fixture(tmp_path):
+ from dataclasses import asdict
+ from harness.web_fetch.models import EvalCase
+ from harness.web_fetch.native_routes import NativeRouteLedger
+ from harness.web_fetch.providers.base import build_provider
+ provider = build_provider('tavily', {'type': 'tavily_extract', 'api_key': 'fixture',
+ 'uniform_native_parameters': True})
+ cases = [EvalCase(name, name, 'https://example.test/'+name, 'control', [],
+ source_benchmark={'input_media_kind': 'html_page'})
+ for name in ('one', 'two')]
+ ledger = NativeRouteLedger(cases, [provider])
+ binding = ledger.write(tmp_path, dataset_sha256='dataset', runtime_source_sha256='runtime')
+ manifest = {'native_routes_required': True, 'native_route_contract': binding,
+ 'dataset_sha256': 'dataset', 'runtime_source_sha256': 'runtime',
+ 'providers': [{'name': provider.name, 'fixed_native_parameters': True}]}
+ return provider, cases, ledger, manifest, {'cases': [asdict(c) for c in cases]}
+
+
+def test_actual_native_http_requests_are_locked_before_transport(tmp_path):
+ import json
+ from datetime import datetime, timezone
+ from unittest.mock import patch
+ from harness.web_fetch.native_routes import verify_native_route_contract
+ from harness.web_fetch.providers.http_client import HttpResponse
+ provider, cases, ledger, manifest, dataset = _route_http_fixture(tmp_path)
+ records = []
+ seen = []
+ def transport(endpoint, **kwargs):
+ assert (tmp_path/'native-route-contract.json').exists()
+ seen.append(kwargs['json_body'])
+ return HttpResponse(200, endpoint, {}, json.dumps({'results': [
+ {'url': kwargs['json_body']['urls'][0], 'raw_content': 'answer'}]}).encode())
+ with patch('harness.web_fetch.providers.tavily.request', side_effect=transport):
+ for case in cases:
+ started = datetime.now(timezone.utc).isoformat()
+ result = provider.fetch(case, 45)
+ records.append({'provider': provider.name, 'case_id': case.id,
+ 'url': case.url, 'started_at': started, 'ok': result.ok,
+ 'provider_metadata': {'native_route_receipt': ledger.receipt(provider, case)}})
+ assert len(seen) == 2 and seen[0]['urls'] != seen[1]['urls']
+ assert len(ledger.observed) == 1
+ assert all('fixture' not in str(row['provider_metadata']) for row in records)
+ assert verify_native_route_contract(tmp_path, manifest, dataset, records)
+
+
+@pytest.mark.parametrize('defect', ['endpoint', 'parameters', 'input_url', 'configuration'])
+def test_native_request_drift_is_rejected_before_second_transport(tmp_path, defect):
+ from unittest.mock import Mock
+ provider, cases, ledger, _, _ = _route_http_fixture(tmp_path)
+ callback = Mock(return_value=object())
+ endpoint = 'https://api.tavily.com/extract'
+ kwargs = {'method': 'POST', 'json_body': {'urls': [cases[0].url], 'format': 'markdown', 'extract_depth': 'advanced', 'include_images': False},
+ 'headers': {'Authorization': 'Bearer fixture'}, 'timeout': 45, 'max_bytes': 20000000}
+ provider.native_http_request(cases[0], callback, endpoint, **kwargs)
+ kwargs['json_body'] = {'urls': [cases[1].url], 'format': 'markdown', 'extract_depth': 'advanced', 'include_images': False}
+ if defect == 'endpoint': endpoint = 'https://api.tavily.com/other'
+ if defect == 'parameters': kwargs['json_body']['format'] = 'html'
+ if defect == 'input_url': kwargs['json_body']['urls'] = ['https://wrong.test/']
+ if defect == 'configuration': provider.config['include_images'] = True
+ with pytest.raises(ValueError, match='Native'):
+ provider.native_http_request(cases[1], callback, endpoint, **kwargs)
+ assert callback.call_count == 1
+ assert ledger.errors
+
+
+@pytest.mark.parametrize('defect', ['missing_receipt', 'signature', 'media', 'freeze', 'scope'])
+def test_release_check_rejects_changed_native_route_evidence(tmp_path, defect):
+ from copy import deepcopy
+ from datetime import datetime, timezone
+ from unittest.mock import Mock
+ from harness.web_fetch.native_routes import verify_native_route_contract
+ provider, cases, ledger, manifest, dataset = _route_http_fixture(tmp_path)
+ records = []
+ for case in cases:
+ provider.native_http_request(case, Mock(), 'https://api.tavily.com/extract',
+ method='POST', json_body={'urls': [case.url], 'format': 'markdown',
+ 'extract_depth': 'advanced', 'include_images': False},
+ headers={'Authorization': 'Bearer fixture'}, timeout=45, max_bytes=20000000)
+ records.append({'provider': provider.name, 'case_id': case.id, 'url': case.url,
+ 'started_at': datetime.now(timezone.utc).isoformat(), 'ok': True,
+ 'provider_metadata': {'native_route_receipt': ledger.receipt(provider, case)}})
+ records = deepcopy(records)
+ if defect == 'missing_receipt': records[0]['provider_metadata'] = {}
+ if defect == 'signature': records[0]['provider_metadata']['native_route_receipt']['requests'][0]['sha256'] = '0'*64
+ if defect == 'media': dataset['cases'][0]['source_benchmark']['input_media_kind'] = 'direct_pdf'
+ if defect == 'freeze': records[0]['started_at'] = '2000-01-01T00:00:00+00:00'
+ if defect == 'scope': records.pop()
+ with pytest.raises(ValueError, match='Native'):
+ verify_native_route_contract(tmp_path, manifest, dataset, records)
+
+
+def test_runner_freezes_route_matrix_before_mocked_provider_calls(tmp_path):
+ import json
+ from dataclasses import asdict
+ from unittest.mock import patch
+ from harness.web_fetch.models import EvalCase
+ from harness.web_fetch.providers.base import build_provider
+ from harness.web_fetch.providers.http_client import HttpResponse
+ from harness.web_fetch.runner import run_evaluation
+ cases = [EvalCase(label, label, 'https://example.test/'+label, 'control', [],
+ source_benchmark={'input_media_kind': 'html_page'}) for label in ('one', 'two')]
+ dataset = {'tool': 'web_fetch', 'id': 'fixture', 'cases': [asdict(c) for c in cases]}
+ source = tmp_path/'dataset.json'
+ source.write_text(json.dumps(dataset))
+ provider = build_provider('tavily', {'type': 'tavily_extract', 'api_key': 'fixture',
+ 'uniform_native_parameters': True})
+ output = tmp_path/'run'
+ def call(endpoint, **kwargs):
+ frozen = json.loads((output/'native-route-contract.json').read_text())
+ assert frozen['pre_call_frozen'] and len(frozen['cases']) == 2
+ target = kwargs['json_body']['urls'][0]
+ return HttpResponse(200, endpoint, {}, json.dumps({'results': [
+ {'url': target, 'raw_content': 'complete answer'}]}).encode())
+ with patch('harness.web_fetch.providers.tavily.request', side_effect=call) as transport:
+ records, _ = run_evaluation(output_dir=output, dataset_meta=dataset, dataset_path=source,
+ cases=cases, providers=[provider], provider_config_path=tmp_path/'providers.json',
+ runs=1, timeout=45, delay=0, reference_policy='frozen_existing')
+ assert transport.call_count == 2
+ assert len(records) == 2
+ manifest = json.loads((output/'manifest.json').read_text())
+ assert manifest['native_routes_required'] and not manifest['native_route_errors']
+ assert all(r['provider_metadata']['native_route_receipt']['requests'] for r in records)
+
+
+def test_jsonl_worker_receives_declared_media_and_records_actual_packet(tmp_path):
+ import sys
+ from harness.web_fetch.models import EvalCase
+ from harness.web_fetch.native_routes import NativeRouteLedger
+ from harness.web_fetch.providers.base import build_provider
+ script = ('import sys,json\nfor line in sys.stdin:\n'
+ ' r=json.loads(line);print(json.dumps({"ok":True,"content":r["input_media_kind"]}),flush=True)\n')
+ provider = build_provider('worker', {'type': 'subprocess_jsonl', 'command': [sys.executable, '-u', '-c', script],
+ 'repo_path': str(tmp_path), 'uniform_native_parameters': True})
+ case = EvalCase('case', 'Case', 'https://example.test/no-extension', 'control', [], track='document',
+ source_benchmark={'input_media_kind': 'direct_pdf'})
+ ledger = NativeRouteLedger([case], [provider], timeout_seconds=5)
+ ledger.write(tmp_path, dataset_sha256='fixture', runtime_source_sha256='fixture')
+ try:
+ result = provider.fetch(case, 5)
+ assert result.ok and result.content == 'direct_pdf'
+ request = ledger.receipt(provider, case)['requests'][0]['signature']['parameters']['request']
+ assert request['input_media_kind'] == 'direct_pdf'
+ assert request['url'] == ''
+ assert str(tmp_path) not in str(request)
+ finally:
+ provider.cancel()
+
+
+@pytest.mark.parametrize('method,module', [
+ ('tavily_extract', 'tavily'), ('exa_contents', 'exa'),
+ ('firecrawl', 'firecrawl'), ('cloudflare_markdown', 'cloudflare'),
+ ('cloudflare_content', 'cloudflare'),
+])
+@pytest.mark.parametrize('track,kind,media', [
+ ('web', 'body', 'html_page'), ('document', 'body', 'direct_pdf'),
+ ('image', 'resource', 'html_page'), ('video', 'resource', 'html_page'),
+ ('file', 'file', 'direct_text'),
+])
+def test_each_http_adapter_matches_its_pre_call_contract(tmp_path, method, module, track, kind, media):
+ from unittest.mock import patch
+ from harness.web_fetch.models import EvalCase
+ from harness.web_fetch.native_routes import NativeRouteLedger
+ from harness.web_fetch.providers.base import build_provider
+ from harness.web_fetch.providers.http_client import HttpResponse
+ config = {'type': method, 'uniform_native_parameters': True,
+ 'api_key': 'fixture', 'api_token': 'fixture', 'account_id': 'fixture',
+ 'include_resource_links': True, 'rich_links_limit': 12, 'include_images': True,
+ 'formats': ['markdown', 'links', 'images'], 'only_main_content': False,
+ 'browser': 'chrome'}
+ provider = build_provider('method', config)
+ case = EvalCase('case', 'Case', 'https://example.test/no-extension', 'control', [],
+ track=track, task_kind=kind, source_benchmark={'input_media_kind': media})
+ ledger = NativeRouteLedger([case], [provider], timeout_seconds=17)
+ ledger.write(tmp_path, dataset_sha256='fixture', runtime_source_sha256='fixture')
+ # Deliberately empty native output: this test proves transport selection,
+ # not retrieval success, and cannot accidentally make a live request.
+ with patch('harness.web_fetch.providers.'+module+'.request',
+ return_value=HttpResponse(200, case.url, {}, b'{}')) as transport:
+ provider.fetch(case, 17)
+ assert transport.call_count == 1
+ receipt = ledger.receipt(provider, case)
+ assert not ledger.errors and len(receipt['requests']) == 1
+ cell = next(iter(ledger.cells.values()))
+ assert receipt['requests'][0]['signature']['parameters'] == cell['request_parameters']
+
+
+def test_custom_firecrawl_without_auth_still_matches_actual_request(tmp_path):
+ from unittest.mock import patch
+ from harness.web_fetch.models import EvalCase
+ from harness.web_fetch.native_routes import NativeRouteLedger
+ from harness.web_fetch.providers.base import build_provider
+ from harness.web_fetch.providers.http_client import HttpResponse
+ provider = build_provider('firecrawl', {'type': 'firecrawl', 'base_url': 'https://fixture.test',
+ 'uniform_native_parameters': True})
+ case = EvalCase('case', 'Case', 'https://example.test/', 'control', [],
+ source_benchmark={'input_media_kind': 'html_page'})
+ ledger = NativeRouteLedger([case], [provider])
+ ledger.write(tmp_path, dataset_sha256='fixture', runtime_source_sha256='fixture')
+ with patch('harness.web_fetch.providers.firecrawl.request',
+ return_value=HttpResponse(200, case.url, {}, b'{}')) as transport:
+ provider.fetch(case, 45)
+ assert transport.call_count == 1 and not ledger.errors
+
+
+def test_contract_survives_aggregation_and_complete_declared_subset(tmp_path):
+ import json
+ from copy import deepcopy
+ from datetime import datetime, timezone
+ from unittest.mock import Mock
+ from harness.web_fetch.native_routes import (
+ materialize_native_route_contracts, verify_native_execution_manifests,
+ )
+ source, output = tmp_path/'source', tmp_path/'aggregate'
+ source.mkdir()
+ output.mkdir()
+ provider, cases, ledger, manifest, dataset = _route_http_fixture(source)
+ records = []
+ for case in cases:
+ provider.native_http_request(case, Mock(), 'https://api.tavily.com/extract',
+ method='POST', json_body={'urls': [case.url], 'format': 'markdown',
+ 'extract_depth': 'advanced', 'include_images': False},
+ headers={'Authorization': 'Bearer fixture'}, timeout=45, max_bytes=20000000)
+ records.append({'attempt_id': case.id, 'provider': provider.name, 'case_id': case.id,
+ 'url': case.url, 'started_at': datetime.now(timezone.utc).isoformat(), 'ok': True,
+ 'provider_metadata': {'native_route_receipt': ledger.receipt(provider, case)}})
+ manifest['attempt_ids'] = [r['attempt_id'] for r in records]
+ original = (source/'native-route-contract.json').read_bytes()
+ materialize_native_route_contracts(output, source, [manifest])
+ assert (output/manifest['native_route_contract']['path']).read_bytes() == original
+ assert verify_native_execution_manifests(output, [manifest], dataset, records)
+ subset = deepcopy(manifest)
+ subset['attempt_ids'] = [records[0]['attempt_id']]
+ with pytest.raises(ValueError, match='incomplete result scope'):
+ verify_native_execution_manifests(output, [subset], dataset, records)
+ subset['projection'] = {'kind': 'complete-provider-case-subset-from-retained-run'}
+ assert verify_native_execution_manifests(output, [subset], dataset, records)
+ # Even a self-consistent forged request hash cannot change frozen parameters.
+ from harness.web_fetch.native_routes import _digest
+ bad = deepcopy(records)
+ item = bad[0]['provider_metadata']['native_route_receipt']['requests'][0]
+ item['signature']['parameters']['body']['format'] = 'html'
+ item['sha256'] = _digest(item['signature'])
+ with pytest.raises(ValueError, match='frozen interface'):
+ verify_native_execution_manifests(output, [manifest], dataset, bad)
+ contract = output/manifest['native_route_contract']['path']
+ doc = json.loads(contract.read_text())
+ doc['pre_call_frozen'] = False
+ contract.write_text(json.dumps(doc))
+ with pytest.raises(ValueError, match='hash changed'):
+ verify_native_execution_manifests(output, [manifest], dataset, records)
+ with pytest.raises(ValueError, match='bytes changed'):
+ materialize_native_route_contracts(tmp_path/'another', output, [deepcopy(manifest)])
+
+
+@pytest.mark.parametrize('media,kind', [('html_page', 'body'), ('direct_pdf', 'body'),
+ ('html_page', 'resource'), ('direct_text', 'file')])
+def test_moli_actual_cli_obeys_declared_route_without_network(tmp_path, media, kind):
+ import hashlib
+ import sys
+ from harness.web_fetch.models import EvalCase
+ from harness.web_fetch.native_routes import NativeRouteLedger
+ from harness.web_fetch.providers.base import build_provider
+ binary = tmp_path/'fake-moli'
+ binary.write_text('#!'+sys.executable+'\nimport sys\n'
+ 'print("moli 1.1.4" if sys.argv[1:]==["--version"] else "answer")\n')
+ binary.chmod(0o700)
+ provider = build_provider('moli', {'type': 'moli_webfetch', 'binary': str(binary),
+ 'expected_reported_version': 'moli 1.1.4',
+ 'expected_binary_sha256': hashlib.sha256(binary.read_bytes()).hexdigest(),
+ 'uniform_native_parameters': True, 'block_private_networks': False})
+ assert provider.validate() == []
+ case = EvalCase('case', 'Case', 'https://example.test/no-extension', 'control', [],
+ track='document' if kind=='body' else 'image' if kind=='resource' else 'file',
+ task_kind=kind, source_benchmark={'input_media_kind': media})
+ ledger = NativeRouteLedger([case], [provider], timeout_seconds=5)
+ ledger.write(tmp_path, dataset_sha256='fixture', runtime_source_sha256='fixture')
+ provider.fetch(case, 5)
+ assert not ledger.errors and len(ledger.receipt(provider, case)['requests']) == 1
+
+
+def test_route_verifier_preserves_optional_dataset_defaults(tmp_path):
+ from datetime import datetime, timezone
+ from unittest.mock import Mock
+ from harness.web_fetch.native_routes import verify_native_route_contract
+ provider, cases, ledger, manifest, dataset = _route_http_fixture(tmp_path)
+ records = []
+ for case in cases:
+ provider.native_http_request(case, Mock(), 'https://api.tavily.com/extract',
+ method='POST', json_body={'urls': [case.url], 'format': 'markdown',
+ 'extract_depth': 'advanced', 'include_images': False},
+ headers={'Authorization': 'Bearer fixture'}, timeout=45, max_bytes=20000000)
+ records.append({'provider': provider.name, 'case_id': case.id, 'url': case.url,
+ 'started_at': datetime.now(timezone.utc).isoformat(), 'ok': True,
+ 'provider_metadata': {'native_route_receipt': ledger.receipt(provider, case)}})
+ for case in dataset['cases']:
+ del case['track']
+ del case['task_kind']
+ assert verify_native_route_contract(tmp_path, manifest, dataset, records)
diff --git a/tests/test_scoring.py b/tests/test_scoring.py
index 5dcb728f4..8a74aec1f 100644
--- a/tests/test_scoring.py
+++ b/tests/test_scoring.py
@@ -699,6 +699,7 @@ def setUp(self):
"legacy": LINKS, "unverified": LINKS.replace("Alpha", "Different"),
"redirected": "[Redirected story](item/1)"}
self.provider = Mock()
+ self.provider.config = {}
self.provider.name, self.provider.type, self.provider.min_interval_seconds = "p", "fixture", 0
self.provider.validate.return_value = []
self.provider.fetch.side_effect = lambda c, timeout: ProviderResult(
diff --git a/tests/test_stable_task_lifecycle.py b/tests/test_stable_task_lifecycle.py
index 389a224cd..37b46fefc 100644
--- a/tests/test_stable_task_lifecycle.py
+++ b/tests/test_stable_task_lifecycle.py
@@ -43,6 +43,7 @@ def test_runner_scores_new_values_and_binds_rule_not_old_answer(self):
path = root/'dataset.json'
path.write_text(json.dumps(dataset))
provider = Mock()
+ provider.config = {}
provider.name, provider.type, provider.min_interval_seconds = 'p', 'synthetic', 0
provider.validate.return_value = []
provider.fetch.return_value = ProviderResult(ok=True, content='# Catalog\n\nNew item\n$99')
From 4e140a1df84a93ce916f45a1cc935e91cd3045a2 Mon Sep 17 00:00:00 2001
From: Maisiqi <324751762+Maisiqi@users.noreply.github.com>
Date: Fri, 16 Oct 2026 02:05:03 +0800
Subject: [PATCH 33/50] Create separately bound post-score public evidence
views
---
.agents/skills/benchmark-report/SKILL.md | 2 +-
harness/web_fetch/README.md | 11 ++
harness/web_fetch/publication_privacy.py | 122 +++++++++++++++++++++++
tests/test_public_evidence_views.py | 106 ++++++++++++++++++++
4 files changed, 240 insertions(+), 1 deletion(-)
create mode 100644 tests/test_public_evidence_views.py
diff --git a/.agents/skills/benchmark-report/SKILL.md b/.agents/skills/benchmark-report/SKILL.md
index c34c53439..a7badf6da 100644
--- a/.agents/skills/benchmark-report/SKILL.md
+++ b/.agents/skills/benchmark-report/SKILL.md
@@ -24,7 +24,7 @@ Public report CLIs default to English where supported and expose `--language en|
5. Separate retrieval success, task acceptance, content quality, latency, cost, and freshness. A missing or partial reference makes the affected metric unavailable or scoped; it does not prove success or failure. Do not derive binary acceptance from an unrelated aggregate similarity score.
6. For heterogeneous same-window latency evidence, preserve the full predeclared task-by-method matrix. Show all-call latency on that common denominator separately from successful-call latency on each method's conditional subset. Treat the all-method shared-pass intersection as a diagnostic, never as a post-outcome selector or ranking sample.
7. At aggregate and publication boundaries, copy only explicit hash-bound evidence and public audit files. Reject symlinks and exclude worker trees, host paths, credentials, and internal run logs. Verify primary, auxiliary, delivered-file, and retained retry artifacts before release.
- When a saved-input handoff needs host-metadata redaction, use the shared publication privacy owner. Preserve private originals and distinguish original from public file hashes; rebind dependent metadata references and disclose auxiliary debug-log projections. Never rewrite scored answers, references or response payloads as a privacy repair. Verify unchanged verdicts, timings and review decisions, and rebuild from the public inputs. Keep regenerated package/report wrappers outside the original-input projection receipt so normal rebuilding does not invalidate it or create circular hashes.
+ When a saved-input handoff needs host-metadata redaction, use the shared publication privacy owner. Preserve private originals and distinguish original from public file hashes; rebind dependent metadata references and disclose auxiliary debug-log projections. Never rewrite original scored answers, references or response payloads as a privacy repair. When a scored answer or native envelope itself contains private values, create a separately identified post-score public view through the shared privacy owner, verify its native-field projection and unchanged task verdict, and disclose that omitted original bytes cannot be reconstructed from the public copy. Verify unchanged verdicts, timings and review decisions, and rebuild from the public inputs. Keep regenerated package/report wrappers outside the original-input projection receipt so normal rebuilding does not invalidate it or create circular hashes.
8. Independent result review is opt-in: do not start it unless the user explicitly requests review. When requested without a broader scope, review machine failures only; review machine passes or every attempt only when the user explicitly asks for full review. Label a review performed with known machine outcomes as an evidence audit, separately from independent blind-review coverage; a fresh review-origin tag alone does not establish blindness. Keep the initial verdict blind, but do not equate agreement with correctness: run the anomaly investigation in [audit and revision](references/audit-and-revision.md) for triggered results before calling the review complete. For a large independent review that is safely batchable, prefer resumable Codex CLI jobs with an exact model ID, read-only sandbox, strict output schema, non-overlapping packets, and bounded concurrency; persist each completed packet so a retry never repeats substantive review. Record the exact reviewed/total coverage, accepted unreviewed passes, reviewer, disagreements, adjudications, anomalies, and unresolved items. A review summary never replaces per-attempt evidence.
9. Rerun only calls whose inputs, provider route/version, runtime contract, or source validity changed. Recompute downstream scores and reports without provider calls when only scoring, annotations, or presentation changed. Never retry merely to obtain a better result. A reference or verdict correction must identify the shared root cause and add executable positive/negative controls through the production scorer; bind those regression IDs in the revision receipt instead of patching stored verdicts.
10. Build the public report from the frozen evidence. Every page should answer a reader question. Show task and run identifiers, the exact expected-versus-actual evidence behind a verdict, denominators and unavailable metrics, method differences, limits, concise reproduction instructions, and a compact review history.
diff --git a/harness/web_fetch/README.md b/harness/web_fetch/README.md
index c2a4299a4..c0c8a853d 100644
--- a/harness/web_fetch/README.md
+++ b/harness/web_fetch/README.md
@@ -653,6 +653,17 @@ product retrieval failure. Full native tool/worker envelopes are retained privat
with hashes, separately from scored answer text. Historical outputs without those
envelopes remain insufficient for an exact raw-response audit.
+When those retained bytes contain private host paths or credentials, the shared
+`publication_privacy.project_evidence_view` creates a separate post-score copy
+under `public-evidence/`. Keep the originals private. The bound receipt records
+original and public hashes and byte sizes; `require_public_evidence_view` checks
+the delivered copy without claiming it can reconstruct omitted values. Native
+JSON envelopes are projected structurally to preserve field types. Before using
+such views in a handoff, verify that the public native-field projection still
+matches the public answer and that rescoring it preserves the task verdict.
+Do not replace an original scored binding with a public-view hash or describe a
+redacted reproduction as byte-for-byte recovery of the private call.
+
For native caller comparisons, `dns_mode` is explicitly `system` (default) or
`public_doh`. All six local methods (Moli, Crawl4AI, OpenClaw, DeepSeek, CowAgent and
diff --git a/harness/web_fetch/publication_privacy.py b/harness/web_fetch/publication_privacy.py
index 855292198..f8fb2eb6d 100644
--- a/harness/web_fetch/publication_privacy.py
+++ b/harness/web_fetch/publication_privacy.py
@@ -354,3 +354,125 @@ def require_host_metadata_projection(source, binding):
if public.stat().st_size != item['published_bytes']:
raise ValueError('Published metadata size differs from projection receipt')
return receipt
+
+
+def project_evidence_view(name, data, *, role, host_prefixes=(), host_names=()):
+ """Create a separately identified public view after scoring.
+
+ The caller retains original bytes privately. This function never changes a
+ result, score, original binding or source file. Its receipt identifies both
+ versions; verifying the public hash does not verify unavailable originals.
+ JSON envelopes are traversed structurally so escaped paths are redacted
+ without corrupting strings or rewriting numeric measurements.
+ """
+ path = PurePosixPath(name)
+ if (path.is_absolute() or not path.parts or path.parts[0] != 'public-evidence'
+ or '..' in path.parts or any(part.startswith('.') for part in path.parts)):
+ raise ValueError('Public evidence views require a separate safe public-evidence path')
+ if role not in {'scored_answer', 'native_response'}:
+ raise ValueError('Unknown public evidence role')
+ if any(not p.startswith('/') or p == '/' for p in host_prefixes):
+ raise ValueError('Public evidence host prefixes must be explicit non-root paths')
+ if any(not host for host in host_names):
+ raise ValueError('Public evidence host names must be explicit nonempty identities')
+ counts = {}
+ patterns = [re.compile(re.escape(p.rstrip('/')) + r'''(?=/|$)[^\s"'<>\\\x00]*''')
+ for p in sorted(set(host_prefixes), key=len, reverse=True)]
+ # User-home paths in retained diagnostics need a complete replacement, not
+ # just a username replacement that would disclose the remaining directories.
+ local = re.compile(LOCAL_USER_PATH.pattern + rb'''[^\s"'<>\\\x00]*''')
+
+ def text_view(value):
+ before = value
+ for pattern in patterns:
+ value, count = pattern.subn('[HOST_PATH_OMITTED]', value)
+ counts['local_path'] = counts.get('local_path', 0) + count
+ for host in host_names:
+ count = value.count(host)
+ value = value.replace(host, '[HOSTNAME_OMITTED]')
+ counts['hostname'] = counts.get('hostname', 0) + count
+ raw = value.encode('utf-8')
+ raw, count = local.subn(b'[HOST_PATH_OMITTED]', raw)
+ counts['local_path'] = counts.get('local_path', 0) + count
+ raw, count = OTHER_SECRET.subn(b'[REDACTED_CREDENTIAL]', raw)
+ counts['credential_shape'] = counts.get('credential_shape', 0) + count
+ projected, receipt = project_file(name, raw)
+ if receipt:
+ counts['sensitive_values'] = counts.get('sensitive_values', 0) + receipt['replacements']
+ result = projected.decode('utf-8')
+ return result if result != before else before
+
+ def node_view(value):
+ if isinstance(value, dict):
+ result = {}
+ for key, item in value.items():
+ public_key = text_view(key)
+ if public_key in result:
+ raise ValueError('Public view would merge distinct native fields')
+ result[public_key] = node_view(item)
+ return result
+ if isinstance(value, list):
+ return [node_view(item) for item in value]
+ return text_view(value) if isinstance(value, str) else value
+
+ text = data.decode('utf-8')
+ try:
+ native = json.loads(text)
+ except ValueError:
+ projected = text_view(text).encode('utf-8')
+ encoding = 'utf-8-text'
+ else:
+ public = node_view(native)
+ projected = (json.dumps(public, ensure_ascii=False, separators=(',', ':')).encode('utf-8')
+ if public != native else data)
+ encoding = 'json'
+ risk = publication_risk(projected)
+ if risk:
+ raise ValueError('Public evidence still has a publication risk: '+risk)
+ receipt = {'kind': 'post_score_public_evidence_view', 'role': role, 'path': name,
+ 'encoding': encoding, 'provider_calls_added': 0,
+ 'original_sha256': hashlib.sha256(data).hexdigest(),
+ 'original_bytes': len(data),
+ 'published_sha256': hashlib.sha256(projected).hexdigest(),
+ 'published_bytes': len(projected),
+ 'redactions': {key: count for key, count in counts.items() if count},
+ 'original_retained_privately': True,
+ 'meaning': 'Public-view bytes are separately identified; original scoring bytes and measurements are unchanged. The public view cannot reconstruct omitted private values.'}
+ return projected, receipt
+
+
+def require_public_evidence_view(source, binding):
+ """Verify a declared public view, without claiming original bytes are public."""
+ from .local_revision import read_bound
+ receipt = json.loads(read_bound(source, binding).read_text())
+ name = receipt.get('path', '')
+ path = PurePosixPath(name)
+ redactions = receipt.get('redactions')
+ if (receipt.get('kind') != 'post_score_public_evidence_view'
+ or receipt.get('role') not in {'scored_answer', 'native_response'}
+ or receipt.get('encoding') not in {'json', 'utf-8-text'}
+ or receipt.get('provider_calls_added') != 0
+ or receipt.get('original_retained_privately') is not True
+ or path.is_absolute() or not path.parts or path.parts[0] != 'public-evidence'
+ or '..' in path.parts or any(p.startswith('.') for p in path.parts)
+ or not isinstance(redactions, dict)
+ or any(key not in {'local_path', 'hostname', 'credential_shape', 'sensitive_values'}
+ or type(count) is not int or count < 1 for key, count in redactions.items())):
+ raise ValueError('Invalid separately identified public evidence view')
+ for label in ('original', 'published'):
+ if (not re.fullmatch('[0-9a-f]{64}', str(receipt.get(label+'_sha256', '')))
+ or type(receipt.get(label+'_bytes')) is not int or receipt[label+'_bytes'] < 0):
+ raise ValueError('Invalid original/public evidence identity')
+ same = receipt['original_sha256'] == receipt['published_sha256']
+ if (same and (redactions or receipt['original_bytes'] != receipt['published_bytes'])
+ or not same and not redactions):
+ raise ValueError('Public view identity does not match its declared redactions')
+ public = read_bound(source, {'path': name, 'sha256': receipt['published_sha256']})
+ raw = public.read_bytes()
+ if len(raw) != receipt['published_bytes'] or publication_risk(raw):
+ raise ValueError('Public evidence view bytes or privacy state differ')
+ if receipt['encoding'] == 'json':
+ json.loads(raw)
+ else:
+ raw.decode('utf-8')
+ return receipt
diff --git a/tests/test_public_evidence_views.py b/tests/test_public_evidence_views.py
new file mode 100644
index 000000000..474098f8e
--- /dev/null
+++ b/tests/test_public_evidence_views.py
@@ -0,0 +1,106 @@
+import hashlib
+import json
+
+import pytest
+
+from harness.web_fetch.publication_privacy import project_evidence_view
+from harness.web_fetch.response_input import project_native_answer
+
+
+def test_native_envelope_view_redacts_decoded_paths_without_changing_other_fields():
+ original = json.dumps({'content': 'Article\nSaved to: /Users/example/private/run/body.txt\nDone',
+ 'elapsed_ms': 1234.5, 'count': 7, 'success': True}, ensure_ascii=True).encode()
+ declaration = {'schema_version': 1, 'content_pointer': ['content'], 'content_format': 'text'}
+ scored, _ = project_native_answer(json.loads(original), declaration)
+ public, receipt = project_evidence_view('public-evidence/native/response.json', original,
+ role='native_response')
+ payload = json.loads(public)
+ assert payload['elapsed_ms'] == 1234.5 and payload['count'] == 7 and payload['success'] is True
+ assert payload['content'] == 'Article\nSaved to: [HOST_PATH_OMITTED]\nDone'
+ assert 'private/run' not in public.decode()
+ assert '/Users/example' in scored
+ assert receipt['original_sha256'] == hashlib.sha256(original).hexdigest()
+ assert receipt['published_sha256'] == hashlib.sha256(public).hexdigest()
+ assert receipt['original_sha256'] != receipt['published_sha256']
+ assert receipt['provider_calls_added'] == 0
+
+
+def test_answer_and_envelope_public_views_preserve_native_field_projection():
+ raw = json.dumps({'body': 'Answer\nSaved to: /Users/example/private/file.md\nEnd'}).encode()
+ declaration = {'schema_version': 1, 'content_pointer': ['body'], 'content_format': 'text'}
+ answer, _ = project_native_answer(json.loads(raw), declaration)
+ public_response, _ = project_evidence_view('public-evidence/response.json', raw, role='native_response')
+ public_answer, receipt = project_evidence_view('public-evidence/answer.txt', answer.encode(), role='scored_answer')
+ assert project_native_answer(json.loads(public_response), declaration)[0].encode() == public_answer
+ assert receipt['role'] == 'scored_answer'
+ assert receipt['original_sha256'] == hashlib.sha256(answer.encode()).hexdigest()
+
+
+def test_view_handles_json_escaped_paths_and_retains_clean_original_bytes():
+ raw = br'{"content":"failed at \u002fUsers\u002fexample\u002fprivate\u002frun.txt","count":4}'
+ public, _ = project_evidence_view('public-evidence/response.json', raw, role='native_response')
+ assert json.loads(public)['content'] == 'failed at [HOST_PATH_OMITTED]'
+ clean = b'{ "count": 4, "content": "Normal body" }\n'
+ assert project_evidence_view('public-evidence/response.json', clean, role='native_response')[0] == clean
+
+
+def test_view_hides_explicit_temp_paths_and_credential_shapes():
+ raw = b'Failed at /private/tmp/example/session.log\nkey: ghp_' + b'X'*32
+ public, receipt = project_evidence_view('public-evidence/answer.txt', raw, role='scored_answer',
+ host_prefixes=['/private/tmp'])
+ assert b'/private/tmp' not in public and b'example/session' not in public
+ assert b'ghp_' not in public
+ assert receipt['redactions']['credential_shape'] == 1
+
+
+@pytest.mark.parametrize('name', ['private_artifacts/response.bin', '../response.txt',
+ '/public-evidence/response.txt', 'public-evidence/.env'])
+def test_view_rejects_paths_that_could_confuse_original_and_public_evidence(name):
+ with pytest.raises(ValueError, match='separate safe'):
+ project_evidence_view(name, b'answer', role='scored_answer')
+
+
+def test_view_does_not_accept_unknown_roles_or_binary_guessing():
+ with pytest.raises(ValueError, match='role'):
+ project_evidence_view('public-evidence/a.txt', b'a', role='reference')
+ with pytest.raises(UnicodeDecodeError):
+ project_evidence_view('public-evidence/a.txt', b'\xff\xfe', role='native_response')
+
+
+def test_redaction_cannot_merge_distinct_native_metadata_fields():
+ raw = json.dumps({'/Users/one/a': 1, '/Users/two/b': 2}).encode()
+ with pytest.raises(ValueError, match='merge distinct'):
+ project_evidence_view('public-evidence/response.json', raw, role='native_response')
+
+
+def test_saved_public_view_verifier_rejects_changed_bytes_or_false_identity(tmp_path):
+ from harness.web_fetch.publication_privacy import require_public_evidence_view
+ name = 'public-evidence/answer.txt'
+ public, receipt = project_evidence_view(name, b'Answer /Users/example/private/a.txt', role='scored_answer')
+ path = tmp_path/name
+ path.parent.mkdir()
+ path.write_bytes(public)
+ proof = tmp_path/'view-receipt.json'
+ def binding():
+ proof.write_text(json.dumps(receipt))
+ return {'path': proof.name, 'sha256': hashlib.sha256(proof.read_bytes()).hexdigest()}
+ assert require_public_evidence_view(tmp_path, binding()) == receipt
+ path.write_bytes(b'different')
+ with pytest.raises(ValueError, match='binding changed'):
+ require_public_evidence_view(tmp_path, binding())
+ path.write_bytes(public)
+ receipt['original_sha256'] = receipt['published_sha256']
+ with pytest.raises(ValueError, match='identity does not match'):
+ require_public_evidence_view(tmp_path, binding())
+
+
+def test_public_view_verifier_rejects_symlink_even_with_matching_bytes(tmp_path):
+ from harness.web_fetch.publication_privacy import require_public_evidence_view
+ public, receipt = project_evidence_view('public-evidence/a.txt', b'answer', role='scored_answer')
+ (tmp_path/'public-evidence').mkdir()
+ (tmp_path/'original.txt').write_bytes(public)
+ (tmp_path/'public-evidence/a.txt').symlink_to(tmp_path/'original.txt')
+ raw = json.dumps(receipt).encode()
+ (tmp_path/'receipt.json').write_bytes(raw)
+ with pytest.raises(ValueError):
+ require_public_evidence_view(tmp_path, {'path': 'receipt.json', 'sha256': hashlib.sha256(raw).hexdigest()})
From f43a06ea543fe4bdf3bab7e4685a9dbb77fb44a6 Mon Sep 17 00:00:00 2001
From: Maisiqi <324751762+Maisiqi@users.noreply.github.com>
Date: Fri, 16 Oct 2026 03:54:43 +0800
Subject: [PATCH 34/50] Bind redacted public evidence through frozen report
rebuilds
---
.agents/skills/benchmark-report/SKILL.md | 2 +-
harness/web_fetch/README.md | 10 +-
harness/web_fetch/adjudication_evidence.py | 27 +-
harness/web_fetch/artifacts.py | 30 +-
harness/web_fetch/local_revision.py | 9 +-
harness/web_fetch/publication_privacy.py | 427 ++++++++++++++++++---
harness/web_fetch/report.py | 32 +-
harness/web_fetch/report_build.py | 21 +-
harness/web_fetch/report_check.py | 60 ++-
harness/web_fetch/safety.py | 23 ++
tests/test_frozen_evidence_copy.py | 38 ++
tests/test_public_evidence_presentation.py | 35 ++
tests/test_public_evidence_views.py | 320 +++++++++++++++
13 files changed, 925 insertions(+), 109 deletions(-)
create mode 100644 tests/test_frozen_evidence_copy.py
create mode 100644 tests/test_public_evidence_presentation.py
diff --git a/.agents/skills/benchmark-report/SKILL.md b/.agents/skills/benchmark-report/SKILL.md
index a7badf6da..d4fa7bae1 100644
--- a/.agents/skills/benchmark-report/SKILL.md
+++ b/.agents/skills/benchmark-report/SKILL.md
@@ -24,7 +24,7 @@ Public report CLIs default to English where supported and expose `--language en|
5. Separate retrieval success, task acceptance, content quality, latency, cost, and freshness. A missing or partial reference makes the affected metric unavailable or scoped; it does not prove success or failure. Do not derive binary acceptance from an unrelated aggregate similarity score.
6. For heterogeneous same-window latency evidence, preserve the full predeclared task-by-method matrix. Show all-call latency on that common denominator separately from successful-call latency on each method's conditional subset. Treat the all-method shared-pass intersection as a diagnostic, never as a post-outcome selector or ranking sample.
7. At aggregate and publication boundaries, copy only explicit hash-bound evidence and public audit files. Reject symlinks and exclude worker trees, host paths, credentials, and internal run logs. Verify primary, auxiliary, delivered-file, and retained retry artifacts before release.
- When a saved-input handoff needs host-metadata redaction, use the shared publication privacy owner. Preserve private originals and distinguish original from public file hashes; rebind dependent metadata references and disclose auxiliary debug-log projections. Never rewrite original scored answers, references or response payloads as a privacy repair. When a scored answer or native envelope itself contains private values, create a separately identified post-score public view through the shared privacy owner, verify its native-field projection and unchanged task verdict, and disclose that omitted original bytes cannot be reconstructed from the public copy. Verify unchanged verdicts, timings and review decisions, and rebuild from the public inputs. Keep regenerated package/report wrappers outside the original-input projection receipt so normal rebuilding does not invalidate it or create circular hashes.
+ When a saved-input handoff needs host-metadata redaction, use the shared publication privacy owner. Preserve private originals and distinguish original from public file hashes; rebind dependent metadata references and disclose auxiliary debug-log projections. Never rewrite original scored answers, references or response payloads as a privacy repair. When a scored answer or native envelope itself contains private values, create a separately identified post-score public view through the shared privacy owner, verify its native-field projection and unchanged task verdict, and disclose that omitted original bytes cannot be reconstructed from the public copy. A public frozen presentation must explicitly bind its complete view inventory, verify displayed bytes against those views, keep acquisition gates strict, and distinguish preserved original diagnostics from what redacted text can reproduce. Verify unchanged verdicts, timings and review decisions, and rebuild from the public inputs. Keep regenerated package/report wrappers outside the original-input projection receipt so normal rebuilding does not invalidate it or create circular hashes.
8. Independent result review is opt-in: do not start it unless the user explicitly requests review. When requested without a broader scope, review machine failures only; review machine passes or every attempt only when the user explicitly asks for full review. Label a review performed with known machine outcomes as an evidence audit, separately from independent blind-review coverage; a fresh review-origin tag alone does not establish blindness. Keep the initial verdict blind, but do not equate agreement with correctness: run the anomaly investigation in [audit and revision](references/audit-and-revision.md) for triggered results before calling the review complete. For a large independent review that is safely batchable, prefer resumable Codex CLI jobs with an exact model ID, read-only sandbox, strict output schema, non-overlapping packets, and bounded concurrency; persist each completed packet so a retry never repeats substantive review. Record the exact reviewed/total coverage, accepted unreviewed passes, reviewer, disagreements, adjudications, anomalies, and unresolved items. A review summary never replaces per-attempt evidence.
9. Rerun only calls whose inputs, provider route/version, runtime contract, or source validity changed. Recompute downstream scores and reports without provider calls when only scoring, annotations, or presentation changed. Never retry merely to obtain a better result. A reference or verdict correction must identify the shared root cause and add executable positive/negative controls through the production scorer; bind those regression IDs in the revision receipt instead of patching stored verdicts.
10. Build the public report from the frozen evidence. Every page should answer a reader question. Show task and run identifiers, the exact expected-versus-actual evidence behind a verdict, denominators and unavailable metrics, method differences, limits, concise reproduction instructions, and a compact review history.
diff --git a/harness/web_fetch/README.md b/harness/web_fetch/README.md
index c0c8a853d..8fdb93fbe 100644
--- a/harness/web_fetch/README.md
+++ b/harness/web_fetch/README.md
@@ -662,7 +662,15 @@ JSON envelopes are projected structurally to preserve field types. Before using
such views in a handoff, verify that the public native-field projection still
matches the public answer and that rescoring it preserves the task verdict.
Do not replace an original scored binding with a public-view hash or describe a
-redacted reproduction as byte-for-byte recovery of the private call.
+redacted reproduction as byte-for-byte recovery of the private call. Public
+frozen presentations declare `publication_evidence_views` in their presentation
+binding. The shared gate requires complete current answer/native coverage,
+checks the actual displayed aliases, rejects private artifact trees, and keeps
+ordinary acquisition/aggregation checks strict. Redacted excerpts can verify
+visible text, not original private coordinates. Report rebuilding uses saved
+original scores; changed public text does not reproduce every original text
+quality diagnostic. Disk-backed metadata projection and independent file clones
+keep large histories practical without hard-linking writable outputs to inputs.
For native caller comparisons, `dns_mode` is explicitly `system` (default) or
diff --git a/harness/web_fetch/adjudication_evidence.py b/harness/web_fetch/adjudication_evidence.py
index c58519e55..3ae306ac9 100644
--- a/harness/web_fetch/adjudication_evidence.py
+++ b/harness/web_fetch/adjudication_evidence.py
@@ -2,7 +2,7 @@
import hashlib
import json
-from .safety import contained_path
+from .publication_privacy import read_evidence_binding, evidence_excerpt_matches
TEMPLATE = '核对已有原始返回、冻结要求和相应修正证据;当前结论见评分明细。'
@@ -131,9 +131,8 @@ def require_adjudication_evidence(source, dataset, records, review):
or not proof.get('rule_ids')):
raise ValueError('Overridden review lacks specific rule/evidence: ' + row['attempt_id'])
require_rule_assertions(cases[record['case_id']], record, row, proof)
- raw = contained_path(source, record['artifact_path'], reject_symlinks=True).read_bytes()
- if hashlib.sha256(raw).hexdigest() != proof['content_sha256']:
- raise ValueError('Adjudication answer changed')
+ answer_binding = {'path': record['artifact_path'], 'sha256': proof['content_sha256']}
+ raw = read_evidence_binding(source, answer_binding).read_bytes()
text = raw.decode('utf-8', errors='replace')
samples = proof.get('samples', [])
if not text:
@@ -142,10 +141,7 @@ def require_adjudication_evidence(source, dataset, records, review):
elif not samples:
raise ValueError('Adjudication lacks an answer excerpt')
for sample in samples:
- start, end = sample.get('start'), sample.get('end')
- if (type(start) is not int or type(end) is not int
- or not 0 <= start < end <= len(text)
- or text[start:end] != sample.get('text')):
+ if not evidence_excerpt_matches(source, answer_binding, sample, text):
raise ValueError('Adjudication excerpt differs from original answer')
checked += 1
return checked
@@ -234,17 +230,13 @@ def _require_supplemental_reviews(source, dataset, records, review):
or not row.get('agent_evidence', '').strip()
or row.get('agent_verdict') not in {'pass', 'fail', 'insufficient'}):
raise ValueError('Supplemental review lacks bound task and answer evidence')
- raw = contained_path(source, record['artifact_path'], reject_symlinks=True).read_bytes()
- if hashlib.sha256(raw).hexdigest() != row['content_sha256']:
- raise ValueError('Supplemental review answer changed')
+ answer_binding = {'path': record['artifact_path'], 'sha256': row['content_sha256']}
+ raw = read_evidence_binding(source, answer_binding).read_bytes()
text = raw.decode('utf-8', errors='replace')
if text and not proof.get('samples'):
raise ValueError('Supplemental review lacks an answer excerpt')
for sample in proof.get('samples', []):
- start, end = sample.get('start'), sample.get('end')
- if (type(start) is not int or type(end) is not int
- or not 0 <= start < end <= len(text)
- or text[start:end] != sample.get('text')):
+ if not evidence_excerpt_matches(source, answer_binding, sample, text):
raise ValueError('Supplemental review excerpt differs')
seen.update(identifiers)
if fresh and seen != fresh:
@@ -317,9 +309,8 @@ def require_retained_reviews(source, dataset, records, review):
or opinion.get('final_verdict') not in {'pass', 'fail'}
or row.get('final_verdict') != opinion['final_verdict']):
raise ValueError('Retained review answer, opinion or verdict changed')
- raw = contained_path(source, record['artifact_path'], reject_symlinks=True).read_bytes()
- if hashlib.sha256(raw).hexdigest() != row.get('content_sha256'):
- raise ValueError('Retained review answer changed')
+ read_evidence_binding(source, {'path': record['artifact_path'],
+ 'sha256': row.get('content_sha256')})
fresh = sum(r.get('review_origin') == 'fresh' for r in review['attempts'])
if (review.get('fresh_reviewed_attempts') != fresh
or fresh + len(ids) != len(review['attempts'])):
diff --git a/harness/web_fetch/artifacts.py b/harness/web_fetch/artifacts.py
index 3d35041de..97ca6c3a4 100644
--- a/harness/web_fetch/artifacts.py
+++ b/harness/web_fetch/artifacts.py
@@ -59,7 +59,7 @@ def record_public_call_log_bindings(record: dict[str, Any]) -> list[dict[str, An
for path, (digest, size) in seen.items()]
-def verify_public_call_log(root: Path, binding: dict[str, Any]) -> Path:
+def verify_public_call_log(root: Path, binding: dict[str, Any], *, allow_public_views=False) -> Path:
"""Verify one public call log's containment, bytes, digest and closed schema."""
normalized = public_call_log_binding(binding)
if normalized is None: # pragma: no cover - caller supplied an object
@@ -130,21 +130,29 @@ def verify_public_call_log(root: Path, binding: dict[str, Any]) -> Path:
output_path = Path(str(payload["output"]["path"]))
if not output_path.parts or output_path.parts[0] != "artifacts":
raise ValueError("Public call log output must stay under artifacts")
- output = contained_path(root, output_path)
- if not output.is_file() or output.is_symlink():
- raise ValueError("Public call log output is missing or unsafe")
- output_bytes = output.read_bytes()
claimed = payload["output"]
- if (claimed["bytes"] != len(output_bytes)
- or claimed["sha256"] != hashlib.sha256(output_bytes).hexdigest()
- or claimed["empty"] is not (len(output_bytes) == 0)):
- raise ValueError("Public call log output binding changed")
+ if allow_public_views:
+ from .publication_privacy import read_evidence_binding, original_evidence_size
+ answer = {'path': output_path.as_posix(), 'sha256': claimed['sha256']}
+ read_evidence_binding(root, answer)
+ size = original_evidence_size(root, answer)
+ if claimed['bytes'] != size or claimed['empty'] is not (size == 0):
+ raise ValueError("Public call log original output binding changed")
+ else:
+ output = contained_path(root, output_path)
+ if not output.is_file() or output.is_symlink():
+ raise ValueError("Public call log output is missing or unsafe")
+ output_bytes = output.read_bytes()
+ if (claimed["bytes"] != len(output_bytes)
+ or claimed["sha256"] != hashlib.sha256(output_bytes).hexdigest()
+ or claimed["empty"] is not (len(output_bytes) == 0)):
+ raise ValueError("Public call log output binding changed")
return path
-def verify_record_public_call_logs(root: Path, record: dict[str, Any]) -> None:
+def verify_record_public_call_logs(root: Path, record: dict[str, Any], *, allow_public_views=False) -> None:
for binding in record_public_call_log_bindings(record):
- verify_public_call_log(root, binding)
+ verify_public_call_log(root, binding, allow_public_views=allow_public_views)
def raw_response_binding(metadata: dict[str, Any]) -> dict[str, Any] | None:
diff --git a/harness/web_fetch/local_revision.py b/harness/web_fetch/local_revision.py
index 224c82c2d..5a4e2431c 100644
--- a/harness/web_fetch/local_revision.py
+++ b/harness/web_fetch/local_revision.py
@@ -12,10 +12,8 @@
def read_bound(source, binding):
- path = contained_path(source, binding['path'], reject_symlinks=True)
- if hashlib.sha256(path.read_bytes()).hexdigest() != binding.get('sha256'):
- raise ValueError('Local revision evidence binding changed: ' + binding['path'])
- return path
+ from .publication_privacy import read_evidence_binding
+ return read_evidence_binding(source, binding)
def require_local_task_replacement(source, binding, dataset, records):
@@ -97,12 +95,13 @@ def require_local_revision(source, binding, dataset, records):
def _require_native_answer_correction(source, original, corrected, proof):
"""Verify a derived answer against the same immutable native response."""
from .response_input import project_native_answer
+ from .publication_privacy import original_evidence_size
if proof.get('attempt_id') != original['attempt_id']:
raise ValueError('Native answer correction lost its attempt identity')
native = read_bound(source, proof['native_response'])
metadata = original.get('provider_metadata', {})
if (proof['native_response']['sha256'] != metadata.get('raw_response_sha256')
- or native.stat().st_size != metadata.get('raw_response_bytes')):
+ or original_evidence_size(source, proof['native_response']) != metadata.get('raw_response_bytes')):
raise ValueError('Native answer correction changed its original response')
before = proof['original_artifact']
after = proof['corrected_artifact']
diff --git a/harness/web_fetch/publication_privacy.py b/harness/web_fetch/publication_privacy.py
index f8fb2eb6d..30771c20c 100644
--- a/harness/web_fetch/publication_privacy.py
+++ b/harness/web_fetch/publication_privacy.py
@@ -4,6 +4,7 @@
import json
import re
from copy import deepcopy
+from functools import lru_cache
from pathlib import PurePosixPath, PureWindowsPath
from urllib.parse import unquote_plus
@@ -76,7 +77,7 @@ def project_execution_manifest(manifest: dict) -> dict:
)
JSON_SENSITIVE_VALUE = re.compile(
rb'(?i)(["\']' + SENSITIVE_KEY + rb'["\']\s*:\s*["\'])(?!<|\[)'
- rb'((?:\\.|[^"\'\r\n])*)(["\'])'
+ rb'((?:\\.|[^"\'\r\n])+)(["\'])'
)
LINE_SENSITIVE_VALUE = re.compile(
rb'(?im)(^|[ \t])(' + SENSITIVE_KEY + rb'[ \t]*[:=])'
@@ -98,33 +99,88 @@ def project_execution_manifest(manifest: dict) -> dict:
)
-def _redact_plain_url_queries(text):
- """Redact sensitive parameters in URLs embedded in diagnostic prose."""
- replacements = 0
+_URL_ENTITY = re.compile(r'&(?:\#[xX][0-9A-Fa-f]+;?|\#[0-9]+;?|[A-Za-z][A-Za-z0-9]+;)')
+
- def redact_url(match):
- nonlocal replacements
- url = match.group()
- query = url.partition('?')[2].partition('#')[0]
- if not query:
- return url
- parts = []
- for item in query.split('&'):
- key, separator, value = item.partition('=')
- # HTML attributes can spell a separator as &.
- logical_key = key.removeprefix('amp;')
- decoded_key = unquote_plus(logical_key).casefold()
- decoded_value = unquote_plus(value)
- sensitive = bool(QUERY_SENSITIVE_KEY.fullmatch(decoded_key))
- placeholder = (decoded_value.startswith('<') and decoded_value.endswith('>')
- or decoded_value == '[REDACTED]')
- if separator and sensitive and value and not placeholder:
- item = key + '=[REDACTED]'
- replacements += 1
- parts.append(item)
- return url.replace('?' + query, '?' + '&'.join(parts), 1)
-
- return EMBEDDED_URL.sub(redact_url, text), replacements
+def _url_entity_surface(raw):
+ """Decode URL entities while retaining each character's original span."""
+ units = [(char, index, index+1) for index, char in enumerate(raw)]
+ for _ in range(8):
+ text = ''.join(unit[0] for unit in units)
+ matches = [m for m in _URL_ENTITY.finditer(text) if html.unescape(m.group()) != m.group()]
+ if not matches:
+ return text, units
+ result, cursor = [], 0
+ for match in matches:
+ result.extend(units[cursor:match.start()])
+ start, end = units[match.start()][1], units[match.end()-1][2]
+ result.extend((char, start, end) for char in html.unescape(match.group()))
+ cursor = match.end()
+ result.extend(units[cursor:])
+ units = result
+ raise ValueError('Excessively nested URL entities require explicit privacy review')
+
+
+def _redact_plain_url_queries(text):
+ """Redact actual query values, preserving URL/Markdown/entity syntax."""
+ replacements, cursor, output = 0, 0, []
+ while match := EMBEDDED_URL.search(text, cursor):
+ start, end = match.span()
+ markdown = bool(re.search(r'\]\([ \t]*$', text[max(0, start-100):start]))
+ if markdown:
+ depth = 0
+ for index in range(start, end):
+ if text[index] == '(':
+ depth += 1
+ elif text[index] == ')':
+ if depth == 0:
+ end = index
+ break
+ depth -= 1
+ raw = text[start:end]
+ suffix = ''
+ if not markdown:
+ for opening, closing in (('(', ')'), ('[', ']'), ('{', '}')):
+ while raw.endswith(closing) and raw.count(closing) > raw.count(opening):
+ raw, suffix = raw[:-1], closing + suffix
+ if _URL_ENTITY.search(raw):
+ url, units = _url_entity_surface(raw)
+ stop = next((i for i, ch in enumerate(url) if ch in '\"\'<>\\\x00' or ch.isspace()), len(url))
+ if stop < len(url):
+ # An encoded HTML attribute delimiter is not query content.
+ used = units[stop][1]
+ raw, end, suffix = raw[:used], start+used, ''
+ url, units = url[:stop], units[:stop]
+ else:
+ url, units = raw, None
+ query_start = url.find('?')
+ changes = []
+ if query_start >= 0:
+ query_end = url.find('#', query_start)
+ if query_end < 0:
+ query_end = len(url)
+ position = query_start+1
+ for item in url[position:query_end].split('&'):
+ key, separator, value = item.partition('=')
+ logical_key = key
+ while logical_key.startswith('amp;'):
+ logical_key = logical_key[4:]
+ decoded_key, decoded_value = unquote_plus(logical_key).casefold(), unquote_plus(value)
+ sensitive = bool(QUERY_SENSITIVE_KEY.fullmatch(decoded_key))
+ placeholder = (decoded_value.startswith('<') and decoded_value.endswith('>')
+ or decoded_value == '[REDACTED]')
+ if separator and sensitive and value and not placeholder:
+ left, right = position+len(key)+1, position+len(item)
+ changes.append((units[left][1] if units else left,
+ units[right-1][2] if units else right))
+ replacements += 1
+ position += len(item)+1
+ for left, right in reversed(changes):
+ raw = raw[:left]+'[REDACTED]'+raw[right:]
+ output.extend((text[cursor:start], raw+suffix))
+ cursor = end
+ output.append(text[cursor:])
+ return ''.join(output), replacements
_JSON_STRING = re.compile(r'"(?:\\.|[^"\\\x00-\x1f])*"')
@@ -173,20 +229,75 @@ def auth_matches(data):
return distinct
-def publication_risk(data):
- """Return a public-bundle risk class without exposing the matched value."""
+def _plain_publication_risk(data):
+ """Inspect actual text, excluding serialization delimiters."""
decoded=html.unescape(data.decode('utf-8',errors='surrogateescape')).encode('utf-8',errors='surrogateescape')
if GOOGLE_KEY.search(decoded) or OTHER_SECRET.search(decoded):
return 'credential-shaped value'
if auth_matches(decoded):
return 'sensitive field value'
- if redact_url_queries(decoded.decode('utf-8', errors='surrogateescape'))[1]:
+ # Bare query parameters such as ×tamp must not be interpreted as the
+ # semicolon-less HTML entity ×. Decode explicit named entities and
+ # numeric references for URL inspection while preserving raw separators.
+ text = data.decode('utf-8', errors='surrogateescape')
+ url_surface = re.sub(r'&(?:\#[xX][0-9A-Fa-f]+;?|\#[0-9]+;?|[A-Za-z][A-Za-z0-9]+;)',
+ lambda match: html.unescape(match.group()), text)
+ if redact_url_queries(url_surface)[1]:
return 'sensitive URL query value'
if LOCAL_USER_PATH.search(decoded):
return 'machine-local user path'
return None
+def _scan_publication_risk(data):
+ """Inspect JSON values structurally, otherwise inspect the actual text.
+
+ A serialized quote after a string such as ``Authorization:`` is not an
+ authorization value. Nonempty sensitive fields and real header values still
+ fail closed, including values hidden by JSON string escapes.
+ """
+ try:
+ payload = json.loads(data)
+ except (ValueError, UnicodeDecodeError):
+ return _plain_publication_risk(data)
+
+ def inspect(value, *, secret=False):
+ if isinstance(value, dict):
+ for key, item in value.items():
+ risk = _plain_publication_risk(key.encode('utf-8'))
+ if risk:
+ return risk
+ sensitive = secret or bool(re.fullmatch(SENSITIVE_KEY.decode('ascii'), key, re.I))
+ risk = inspect(item, secret=sensitive)
+ if risk:
+ return risk
+ elif isinstance(value, list):
+ for item in value:
+ risk = inspect(item, secret=secret)
+ if risk:
+ return risk
+ elif isinstance(value, str):
+ if (secret and value and not ((value.startswith('<') and value.endswith('>'))
+ or (value.startswith('[') and value.endswith(']')))):
+ return 'sensitive field value'
+ return _plain_publication_risk(value.encode('utf-8'))
+ return None
+
+ return inspect(payload)
+
+
+@lru_cache(maxsize=8)
+def _cached_publication_risk(data):
+ return _scan_publication_risk(data)
+
+
+def publication_risk(data):
+ """Reuse checks only for identical immutable bytes; bound retained memory."""
+ data = bytes(data)
+ return (_cached_publication_risk(data) if len(data) <= 2_000_000
+ else _scan_publication_risk(data))
+
+
def project_file(name, data):
"""Redact only auxiliary HTML. Any match elsewhere requires human review."""
path = PurePosixPath(name)
@@ -254,7 +365,7 @@ def _host_metadata_projection_path(name):
return metadata or auxiliary_log
-def project_host_metadata_files(files, host_prefixes, *, host_names=()):
+def project_host_metadata_files(files, host_prefixes, *, host_names=(), public_excerpt_views=False, output_store=None):
"""Project host metadata/auxiliary logs and explicitly rebind dependencies.
Inputs and measured payloads are immutable. Returned receipts distinguish
@@ -263,24 +374,33 @@ def project_host_metadata_files(files, host_prefixes, *, host_names=()):
"""
if not host_prefixes or any(not p.startswith('/') or p == '/' for p in host_prefixes):
raise ValueError('Explicit non-root host prefixes are required')
- patterns = [re.compile(re.escape(p.rstrip('/')) + r"(?=/|$)[^\s\"'<>\x00]*")
+ patterns = [re.compile(re.escape(p.rstrip('/')) + r"(?=/|$)[^\s\"'<>\\\x00]*")
for p in sorted(set(host_prefixes), key=len, reverse=True)]
json_names = {name for name in files if name.endswith(('.json', '.jsonl'))}
- parsed = {}
+ original_hashes = {name: hashlib.sha256(files[name]).hexdigest() for name in files}
+ by_hash = {digest: name for name, digest in original_hashes.items()}
+ # Prewarm hash dependencies before parsing a large parent. Disk-backed
+ # mappings can therefore project multi-GB histories one document at a time.
+ dependencies = {}
for name in json_names:
- text = files[name].decode('utf-8')
- parsed[name] = ([json.loads(line) for line in text.splitlines() if line.strip()]
- if name.endswith('.jsonl') else json.loads(text))
- by_hash = {hashlib.sha256(raw).hexdigest(): name for name, raw in files.items()}
- projected, published_hashes, changed_fields, visiting = {}, {}, {}, set()
+ hashes = {match.group(1).decode('ascii') for match in
+ re.finditer(rb'"([0-9a-f]{64})"(?!\s*:)', files[name])}
+ dependencies[name] = {by_hash[digest] for digest in hashes
+ if digest in by_hash and by_hash[digest] != name}
+ projected = output_store if output_store is not None else {}
+ published_hashes, changed_fields, visiting = {}, {}, set()
+ original_sizes, published_sizes, changed_names = {}, {}, set()
def project(name):
- if name in projected:
- return projected[name]
+ if name in published_hashes:
+ return
if name in visiting:
raise ValueError('Cyclic publication metadata dependencies')
visiting.add(name)
- raw = files[name]
+ for dependency in sorted(dependencies.get(name, set())):
+ project(dependency)
+ original = raw = files[name]
+ original_sizes[name] = len(original)
fields = []
def value(node, path=''):
if isinstance(node, dict):
@@ -304,35 +424,46 @@ def value(node, path=''):
if not host:
raise ValueError('Host names must be explicit nonempty identities')
result = result.replace(host, '[HOSTNAME_OMITTED]')
+ if public_excerpt_views and (path.endswith('.samples[].text')
+ or path.endswith('.agent_evidence_quotes[]') or path.endswith('.evidence_quotes[]')):
+ public, _ = project_evidence_view('public-evidence/excerpt.txt', result.encode('utf-8'),
+ role='scored_answer', host_prefixes=host_prefixes, host_names=host_names,
+ content_format='text')
+ result = public.decode('utf-8')
if result != node:
fields.append(path)
return result
- if name in parsed:
- result = value(parsed[name])
- if result != parsed[name]:
+ if name in json_names:
+ text = raw.decode('utf-8')
+ parsed = ([json.loads(line) for line in text.splitlines() if line.strip()]
+ if name.endswith('.jsonl') else json.loads(text))
+ result = value(parsed)
+ if result != parsed:
raw = ((''.join(json.dumps(row, ensure_ascii=False, separators=(',', ':')) + '\n'
for row in result)) if name.endswith('.jsonl')
else json.dumps(result, ensure_ascii=False, indent=2) + '\n').encode()
elif host_names:
raw = value(raw.decode('utf-8'), 'auxiliary_log').encode('utf-8')
- if raw != files[name] and not _host_metadata_projection_path(name):
+ if raw != original and not _host_metadata_projection_path(name):
raise ValueError('Host projection must not alter scored or response payloads')
+ if raw != original:
+ changed_names.add(name)
projected[name] = raw
+ published_sizes[name] = len(raw)
published_hashes[name] = hashlib.sha256(raw).hexdigest()
changed_fields[name] = sorted(set(fields))
visiting.remove(name)
- return raw
for name in files:
project(name)
receipt = {'kind': 'host_metadata_publication_projection',
'provider_calls_added': 0,
'meaning': 'Original identities are retained below; published hashes identify redacted copies only.',
- 'files': [{'path': name, 'original_sha256': hashlib.sha256(files[name]).hexdigest(),
- 'published_sha256': hashlib.sha256(projected[name]).hexdigest(),
- 'original_bytes': len(files[name]), 'published_bytes': len(projected[name]),
+ 'files': [{'path': name, 'original_sha256': original_hashes[name],
+ 'published_sha256': published_hashes[name],
+ 'original_bytes': original_sizes[name], 'published_bytes': published_sizes[name],
'redacted_fields': changed_fields[name]}
- for name in sorted(files) if files[name] != projected[name]]}
+ for name in sorted(changed_names)]}
return projected, receipt
@@ -356,7 +487,7 @@ def require_host_metadata_projection(source, binding):
return receipt
-def project_evidence_view(name, data, *, role, host_prefixes=(), host_names=()):
+def project_evidence_view(name, data, *, role, host_prefixes=(), host_names=(), content_format="auto"):
"""Create a separately identified public view after scoring.
The caller retains original bytes privately. This function never changes a
@@ -371,6 +502,8 @@ def project_evidence_view(name, data, *, role, host_prefixes=(), host_names=()):
raise ValueError('Public evidence views require a separate safe public-evidence path')
if role not in {'scored_answer', 'native_response'}:
raise ValueError('Unknown public evidence role')
+ if content_format not in {'auto', 'text', 'json'}:
+ raise ValueError('Unknown public evidence content format')
if any(not p.startswith('/') or p == '/' for p in host_prefixes):
raise ValueError('Public evidence host prefixes must be explicit non-root paths')
if any(not host for host in host_names):
@@ -409,23 +542,36 @@ def node_view(value):
public_key = text_view(key)
if public_key in result:
raise ValueError('Public view would merge distinct native fields')
- result[public_key] = node_view(item)
+ if (re.fullmatch(SENSITIVE_KEY.decode('ascii'), key, re.I)
+ and isinstance(item, str) and item
+ and not ((item.startswith('<') and item.endswith('>'))
+ or (item.startswith('[') and item.endswith(']')))):
+ result[public_key] = '[REDACTED]'
+ counts['sensitive_values'] = counts.get('sensitive_values', 0) + 1
+ else:
+ result[public_key] = node_view(item)
return result
if isinstance(value, list):
return [node_view(item) for item in value]
return text_view(value) if isinstance(value, str) else value
text = data.decode('utf-8')
- try:
- native = json.loads(text)
- except ValueError:
+ if content_format == 'text':
projected = text_view(text).encode('utf-8')
encoding = 'utf-8-text'
else:
- public = node_view(native)
- projected = (json.dumps(public, ensure_ascii=False, separators=(',', ':')).encode('utf-8')
- if public != native else data)
- encoding = 'json'
+ try:
+ native = json.loads(text)
+ except ValueError:
+ if content_format == 'json':
+ raise ValueError('Declared JSON public evidence is not JSON')
+ projected = text_view(text).encode('utf-8')
+ encoding = 'utf-8-text'
+ else:
+ public = node_view(native)
+ projected = (json.dumps(public, ensure_ascii=False, separators=(',', ':')).encode('utf-8')
+ if public != native else data)
+ encoding = 'json'
risk = publication_risk(projected)
if risk:
raise ValueError('Public evidence still has a publication risk: '+risk)
@@ -476,3 +622,166 @@ def require_public_evidence_view(source, binding):
else:
raw.decode('utf-8')
return receipt
+
+
+@lru_cache(maxsize=4)
+def _public_view_inventory_index(raw):
+ # Cache parsing by exact file bytes, never by a path or timestamp. Every
+ # caller still reads and checks the inventory's declared SHA-256.
+ index = json.loads(raw)
+ if (index.get('kind') != 'post_score_public_evidence_view_inventory'
+ or index.get('provider_calls_added') != 0):
+ raise ValueError('Invalid public evidence view inventory')
+ entries = {}
+ for entry in index.get('files', []):
+ key = entry.get('original_path'), entry.get('original_sha256')
+ if key in entries:
+ raise ValueError('Ambiguous public evidence view identity')
+ entries[key] = entry
+ return entries
+
+
+def bound_public_view(source, binding):
+ """Resolve only a view explicitly bound by this snapshot's public profile.
+
+ A returned view verifies public bytes plus the recorded original identity;
+ it does not claim that omitted original bytes have been reconstructed.
+ """
+ from pathlib import Path
+ from .safety import contained_path
+ source = Path(source)
+ profile_path = contained_path(source, 'presentation-binding.json', reject_symlinks=True)
+ if not profile_path.is_file():
+ return None
+ profile = json.loads(profile_path.read_text())
+ declared = profile.get('publication_evidence_views')
+ if not declared:
+ return None
+ index_path = contained_path(source, declared['path'], reject_symlinks=True)
+ raw = index_path.read_bytes()
+ if hashlib.sha256(raw).hexdigest() != declared.get('sha256'):
+ raise ValueError('Public evidence view inventory hash changed')
+ entries = _public_view_inventory_index(raw)
+ entry = entries.get((binding.get('path'), binding.get('sha256')))
+ if entry is None:
+ return None
+ receipt = require_public_evidence_view(source, entry['view_receipt'])
+ original_root = PurePosixPath(binding['path']).parts[0]
+ expected_root = {'scored_answer': 'artifacts', 'native_response': 'private_artifacts'}[receipt['role']]
+ if original_root != expected_root:
+ raise ValueError('Public payload views cannot replace metadata or source authority')
+ if receipt['original_sha256'] != binding['sha256']:
+ raise ValueError('Public view does not identify the requested original')
+ return contained_path(source, receipt['path'], reject_symlinks=True), receipt
+
+
+def read_evidence_binding(source, binding):
+ """Verify original bytes, or return the explicitly disclosed public view."""
+ from .safety import contained_path
+ path = contained_path(source, binding['path'], reject_symlinks=True)
+ if path.is_file() and hashlib.sha256(path.read_bytes()).hexdigest() == binding.get('sha256'):
+ return path
+ public = bound_public_view(source, binding)
+ if public is None:
+ raise ValueError('Local revision evidence binding changed: ' + binding['path'])
+ return public[0]
+
+
+def original_evidence_size(source, binding):
+ """Return verified original size, or its explicitly recorded private size."""
+ path = read_evidence_binding(source, binding)
+ if hashlib.sha256(path.read_bytes()).hexdigest() == binding['sha256']:
+ return path.stat().st_size
+ public = bound_public_view(source, binding)
+ if public is None:
+ raise ValueError('Public evidence lacks its original-size disclosure')
+ return public[1]['original_bytes']
+
+
+def evidence_excerpt_matches(source, binding, sample, text):
+ """Validate exact original offsets, or a disclosed redacted-view excerpt.
+
+ Original coordinates remain recorded. A redacted public view can verify the
+ visible excerpt's presence, not its former private-byte/character position.
+ """
+ start, end = sample.get('start'), sample.get('end')
+ if type(start) is not int or type(end) is not int or not 0 <= start < end:
+ return False
+ excerpt = sample.get('text')
+ if not isinstance(excerpt, str) or not excerpt:
+ return False
+ if end <= len(text) and text[start:end] == excerpt:
+ return True
+ public = bound_public_view(source, binding)
+ return bool(public and public[1]['redactions'] and excerpt in text)
+
+
+def require_public_evidence_view_inventory(source, binding, records):
+ """Require complete current-answer/native-response view coverage at release."""
+ from .local_revision import read_bound
+ from .safety import contained_path
+ if (source/'private_artifacts').exists():
+ raise ValueError('Public evidence snapshot must not contain private artifact trees')
+ index = json.loads(read_bound(source, binding).read_text())
+ if (index.get('kind') != 'post_score_public_evidence_view_inventory'
+ or index.get('provider_calls_added') != 0):
+ raise ValueError('Invalid public evidence view inventory')
+ entries = index.get('files', [])
+ wanted = {(row['artifact_path'], row['metrics']['content_sha256']) for row in records}
+ for row in records:
+ metadata = row.get('provider_metadata', {})
+ if metadata.get('raw_response_path'):
+ wanted.add((metadata['raw_response_path'], metadata['raw_response_sha256']))
+ identities = set()
+ for entry in entries:
+ identity = entry.get('original_path'), entry.get('original_sha256')
+ if identity in identities:
+ raise ValueError('Public evidence view inventory repeats an original')
+ identities.add(identity)
+ receipt = require_public_evidence_view(source, entry['view_receipt'])
+ root = PurePosixPath(identity[0]).parts
+ expected_root = {'scored_answer': 'artifacts', 'native_response': 'private_artifacts'}[receipt['role']]
+ if (not root or root[0] != expected_root or '..' in root
+ or receipt['original_sha256'] != identity[1]):
+ raise ValueError('Public evidence view inventory changes an original authority')
+ if identity in wanted and receipt['role'] == 'scored_answer':
+ alias = contained_path(source, identity[0], reject_symlinks=True)
+ if (not alias.is_file()
+ or hashlib.sha256(alias.read_bytes()).hexdigest() != receipt['published_sha256']):
+ raise ValueError('Displayed answer differs from its declared public view')
+ if not wanted <= identities:
+ raise ValueError('Public evidence view inventory omits current evidence')
+ return index
+
+
+def public_render_views(source, records):
+ """Read bound display annotations; never rewrite saved result records."""
+ from .local_revision import read_bound
+ profile_path = source/'presentation-binding.json'
+ if not profile_path.is_file():
+ return {}
+ profile = json.loads(profile_path.read_text())
+ declared = profile.get('publication_evidence_views')
+ if not declared:
+ return {}
+ index = json.loads(read_bound(source, declared).read_text())
+ entries = {(e['original_path'], e['original_sha256']): e for e in index['files']}
+ result = {}
+ for row in records:
+ metadata = row.get('provider_metadata', {})
+ wanted = {'scored_answer': (row['artifact_path'], row['metrics']['content_sha256'])}
+ if metadata.get('raw_response_path'):
+ wanted['native_response'] = (metadata['raw_response_path'], metadata['raw_response_sha256'])
+ views = {}
+ for role, identity in wanted.items():
+ if identity not in entries:
+ raise ValueError('Public rendering omits a declared evidence view')
+ binding = entries[identity]['view_receipt']
+ receipt = json.loads(read_bound(source, binding).read_text())
+ if receipt['role'] != role or receipt['original_sha256'] != identity[1]:
+ raise ValueError('Public rendering changed its original evidence identity')
+ views[role] = {key: receipt[key] for key in ('path', 'original_sha256', 'published_sha256',
+ 'original_bytes', 'published_bytes', 'redactions')}
+ views[role]['receipt'] = binding['path']
+ result[row['attempt_id']] = views
+ return result
diff --git a/harness/web_fetch/report.py b/harness/web_fetch/report.py
index 1460f7803..bab784f94 100644
--- a/harness/web_fetch/report.py
+++ b/harness/web_fetch/report.py
@@ -590,6 +590,8 @@ def run_log_section(out,record,output):
payload=logs[-1]['payload'] if logs else {}
process=payload.get('process') if isinstance(payload.get('process'),dict) else {}
timing=payload.get('timing') if isinstance(payload.get('timing'),dict) else {}
+ native_public=metadata.get('publication_views',{}).get('native_response')
+ native_public_link=('
'+link('../'+native_public['path'],'下载完整原生响应(公开副本)','Download complete native response (public view)')+' · '+link('../'+native_public['receipt'],'原件与公开副本校验记录','Original/public identity record')) if native_public else ''
artifact_relative=record.get('artifact_path')
artifact=contained_path(out,artifact_relative) if artifact_relative else None
output_data=artifact.read_bytes() if artifact and artifact.is_file() else output.encode('utf-8')
@@ -662,7 +664,12 @@ def run_log_section(out,record,output):
raw_record=' · '.join(f'下载{esc(log["label"])}记录' for log in logs)
if artifact_relative:
if raw_record:raw_record+=' · '
- raw_record+=f'下载本次保存的原始输出'
+ _answer_view=metadata.get('publication_views',{}).get('scored_answer')
+ if _answer_view and _answer_view['original_sha256']!=_answer_view['published_sha256']:
+ raw_record+=f''+model_text('下载本次输出公开副本','Download public output view')+''
+ else:
+ raw_record+=f'下载本次保存的原始输出'
+ raw_record+=native_public_link
public_response=metadata.get('public_response_path')
if public_response:
response_view=diagnostic_view_page(out,public_response,'完整原始响应','产品实际返回;公开副本已隐藏凭据与本机路径。')
@@ -683,16 +690,17 @@ def run_log_section(out,record,output):
raw_record+=(f'下载完整原始 debug 日志'
f'({diagnostic_size:,} 字节 · SHA-256:{esc(diagnostic_sha)} · 截断:{diagnostic_truncated})')
if not raw_record:raw_record='这次历史调用没有可下载的原始记录'
+ evidence_identity_note=(model_text('评分原件私下保留;公开下载隐藏目录或敏感值,校验记录分别标明原件与公开副本。','Scoring originals remain private; public downloads hide paths or sensitive values and bind original/public identities separately.') if any(view.get('redactions') for view in metadata.get('publication_views',{}).values()) else '原始输出保持调用后保存的字节。')
return (f'运行日志与证据
'
f'
{fields}
'
f'
查看完整运行字段
{full_fields}
'
f'
结论说明:{result_note}
'
f'
查看本次真实日志
{diagnostic}
'
f'
原始运行证据
'
- f'
{raw_record}。运行记录保留请求状态、耗时和证据路径;原始输出保持调用后保存的字节。
')
+ f'{raw_record}。运行记录保留请求状态、耗时和证据路径;{evidence_identity_note}
')
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''+model_text('公开副本已去掉本机目录信息。清单分别记录原始文件与公开副本的校验值;公开校验值不冒充原始调用标识。网页返回、成绩、耗时和复核结论保持不变。','This public copy omits host paths. The manifest distinguishes original and public file hashes; public hashes do not claim to be original call identities. Answers, scores, timings and review decisions are unchanged.')+' '+link('../'+public_profile['publication_projection']['path'],'PUBLICATION-PROJECTION.json')+'
'
+ reproduce+=''+model_text('公开副本已去掉本机目录信息。清单分别记录原始文件与公开副本的校验值;公开校验值不冒充原始调用标识。成绩、耗时和复核结论保持不变。','This public copy omits host paths. The manifest distinguishes original and public file hashes; public hashes do not claim to be original call identities. Scores, timings and review decisions are unchanged.')+' '+link('../'+public_profile['publication_projection']['path'],'PUBLICATION-PROJECTION.json')+'
'
+ if public_profile.get('publication_evidence_views'):
+ reproduce+=''+model_text('评分时的原件私下保留;公开回答和完整原生响应隐藏了目录或敏感值。清单保留评分原件的标识,并单独记录公开副本。公开文件可重建本报告,不能还原隐藏内容;正文质量等指标保留原件的计算值,不保证用脱敏文字逐字重算一致。复核片段的坐标记录原件位置;公开副本仅能核对仍可见的片段。','Scoring originals remain private. Public answers and complete native responses hide paths or sensitive values. The inventory preserves original scoring identities and separately binds public views. Public files rebuild this report but cannot reconstruct omitted content. Text-quality diagnostics retain the original calculations and may not reproduce exactly from redacted text. Review coordinates refer to originals; public views verify only the visible excerpts.')+' '+link('../'+public_profile['publication_evidence_views']['path'],'PUBLIC-EVIDENCE-VIEWS.json')+'
'
support+=section('核心复现指令',reproduce)
write(out,'reproduce/index.html','环境与复现',support)
if not authored_interpretation and not local_matrix:
diff --git a/harness/web_fetch/report_build.py b/harness/web_fetch/report_build.py
index d6c30db3a..bf661fea5 100644
--- a/harness/web_fetch/report_build.py
+++ b/harness/web_fetch/report_build.py
@@ -206,7 +206,7 @@ def _build_frozen_presentation(source, output, presentation_binding, *,
show_local_status_banners):
"""Render a bound historical report using saved scores and public evidence."""
from harness.report_languages import select_report_language
- from .safety import evidence_tree_identity
+ from .safety import evidence_tree_identity, copy_evidence_file
if source in output.parents:
raise ValueError("Frozen presentation output must be outside its source tree")
before = evidence_tree_identity(source)
@@ -224,7 +224,7 @@ def _build_frozen_presentation(source, output, presentation_binding, *,
output.parent.mkdir(parents=True, exist_ok=True)
with tempfile.TemporaryDirectory(prefix=".frozen-presentation-", dir=output.parent) as work:
staging = Path(work) / "report"
- shutil.copytree(source, staging)
+ shutil.copytree(source, staging, copy_function=copy_evidence_file)
shutil.rmtree(staging / "en", ignore_errors=True)
shutil.copy2(presentation_binding, staging / "presentation-binding.json")
batches = json.loads((source / "execution.json").read_text())["batches"]
@@ -1306,6 +1306,9 @@ def validate_local_presentation(output: Path, binding_path: Path):
require_host_metadata_projection(output, binding['publication_projection'])
dataset = json.loads((output / 'dataset.json').read_text())
records = load_jsonl(contained_path(output, 'results.jsonl'))
+ if binding.get('publication_evidence_views'):
+ from .publication_privacy import require_public_evidence_view_inventory
+ require_public_evidence_view_inventory(output, binding['publication_evidence_views'], records)
from .native_routes import verify_native_execution_manifests
execution = output/'execution.json'
if execution.is_file():
@@ -1339,10 +1342,16 @@ def validate_local_presentation(output: Path, binding_path: Path):
or any(item['case_id'] not in excluded for item in reliability['unjudged']))):
raise ValueError('Presentation refresh requires the complete declared matrix')
for row in records:
- artifact = contained_path(output, row['artifact_path'])
- if hashlib.sha256(artifact.read_bytes()).hexdigest() != row['metrics']['content_sha256']:
- raise ValueError('Saved output changed: ' + row['attempt_id'])
- verify_record_public_call_logs(output, row)
+ from .publication_privacy import read_evidence_binding
+ try:
+ read_evidence_binding(output, {'path': row['artifact_path'],
+ 'sha256': row['metrics']['content_sha256']})
+ except ValueError as error:
+ if binding.get('publication_evidence_views'):
+ raise
+ raise ValueError('Saved output changed: ' + row['attempt_id']) from error
+ verify_record_public_call_logs(output, row,
+ allow_public_views=bool(binding.get('publication_evidence_views')))
return binding, dataset, records, manifest
diff --git a/harness/web_fetch/report_check.py b/harness/web_fetch/report_check.py
index b31966cfe..b1b8102f7 100644
--- a/harness/web_fetch/report_check.py
+++ b/harness/web_fetch/report_check.py
@@ -18,6 +18,49 @@
from harness.web_fetch.storage import require_canonical_inputs, require_current_report_profile
+def require_public_evidence_page(content, relative, views):
+ """Require inspectable public downloads and honest original/public hashes."""
+ from bs4 import BeautifulSoup
+ from urllib.parse import unquote, urljoin
+ content = re.sub(r'<(pre|script|style)\b[^>]*>.*?\1>', '', content, flags=re.I | re.S)
+ soup = BeautifulSoup(content, 'html.parser')
+ visible = soup.get_text(' ', strip=True)
+ if '