Skip to content

feat(webfetch): support main and full content acceptance scopes - #268

Open
lanyue-llk wants to merge 10 commits into
mainfrom
codex/moli-main-content
Open

lanyue-llk wants to merge 10 commits into
mainfrom
codex/moli-main-content

Conversation

@lanyue-llk

@lanyue-llk lanyue-llk commented Nov 11, 2026 •

Copy link
Copy Markdown
Contributor

Tasks can declare independently bound main/full acceptance profiles and select them before acquisition or scoring. Rescoring retains the dataset and per-task scopes saved with the run, including filtered mixed scoped/legacy populations. Both profiles preserve the original blocked-page and navigation constraints; unsupported inherited full-page tree checks fail explicitly. Public definitions retain approved reference bindings and omit undeclared evidence payloads.

The maintained diagnostic renderer adds issue guides with hash-bound figures and concrete cases: requirements, observed output, evidenced cause, repair and acceptance checks. Category and issue indexes link to the exact diagnosis, saved result or validated external repair issue. Primary figures lead their sections without folding; release checks reject folded or displaced figures and changed issue destinations. Details distinguish missing headings from insufficient section text, expose target media and file checks, and compare rates when cohort sizes differ. Illustrated input manifests carry their hash-bound PNG assets into the build. Publication retains and rechecks the planned population. Packaged issue filters and the shared guarded evidence loader work through virtual routes; figures use bounded parallel reads.

Validation of the latest presentation change: 43 report-template tests and 82 subtests, including negative controls for hidden/displaced diagrams and unsafe or incorrect issue links; Ruff and diff checks passed. Earlier scope, scoring, CLI, package and navigation checks are recorded in the PR review replies. All 1,060 main/full requirement selections in the report remain identical to the frozen selector; no provider calls were added. The scoring changes belong to the next minor software release under docs/releases.md.

Related deliverables:

Measured responses, report prose and generated figures remain outside Git.

@lanyue-llk
lanyue-llk force-pushed the codex/moli-main-content branch from 9ccc896 to 691cd74 Compare November 11, 2026 04:32

@lexmount-ocr lexmount-ocr Bot left a comment •

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Review complete · 691cd74

This review found 6 issue(s), including 6 blocking issue(s).

function diagnosticAnchor(){let select=document.querySelector('select[data-filter="issue"]');if(!select)return;let match=location.hash.match(/^#issue=([a-z]+)$/);if(!match||![...select.options].some(o=>o.value===match[1]))return;document.querySelectorAll('select[data-filter]').forEach(s=>s.value=s.dataset.filter==='result'?'fail':'');select.value=match[1];cascadeFilters();document.getElementById('failure-inventory')?.scrollIntoView()}
document.addEventListener('DOMContentLoaded',()=>{document.querySelectorAll('[data-call-log-open]').forEach(b=>b.addEventListener('click',()=>{openCallLog(b)}));document.querySelectorAll('[data-call-log-close]').forEach(b=>b.addEventListener('click',()=>closeCallLog(b)));diagnosticAnchor()});window.addEventListener('hashchange',diagnosticAnchor); No newline at end of file
async function loadDiagnosticEvidence(dialog){for(let pre of dialog.querySelectorAll('pre[data-evidence-file]')){if(pre.dataset.loaded)continue;try{if(typeof textFile==='function'){pre.textContent=await textFile(pre.dataset.evidenceFile)}else{let response=await fetch(pre.dataset.evidenceFile);if(!response.ok)throw Error('HTTP '+response.status);pre.textContent=await response.text();}pre.dataset.loaded='1'}catch(error){pre.textContent='Evidence could not be loaded: '+error.message;}}}
function initDiagnostic(){document.querySelectorAll('[data-call-log-open]').forEach(b=>b.addEventListener('click',()=>{openCallLog(b);loadDiagnosticEvidence(document.getElementById(b.dataset.callLogOpen))}));document.querySelectorAll('[data-call-log-close]').forEach(b=>b.addEventListener('click',()=>closeCallLog(b)));diagnosticAnchor()}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

bug · medium
P2: Each evidence click now starts two loads. The shared openCallLog() in assets/report.js already calls loadDiagnosticEvidence(dialog), and this handler immediately calls it again. The new function overrides the shared loader and checks only dataset.loaded, which is set after awaiting the fetch, so both invocations fetch/decode the same large evidence concurrently. Keep a single invocation and retain the shared loader's in-flight guard (and large-text handling), adding package textFile support there rather than overriding it.

document.addEventListener('DOMContentLoaded',()=>{document.querySelectorAll('[data-call-log-open]').forEach(b=>b.addEventListener('click',()=>{openCallLog(b)}));document.querySelectorAll('[data-call-log-close]').forEach(b=>b.addEventListener('click',()=>closeCallLog(b)));diagnosticAnchor()});window.addEventListener('hashchange',diagnosticAnchor); No newline at end of file
async function loadDiagnosticEvidence(dialog){for(let pre of dialog.querySelectorAll('pre[data-evidence-file]')){if(pre.dataset.loaded)continue;try{if(typeof textFile==='function'){pre.textContent=await textFile(pre.dataset.evidenceFile)}else{let response=await fetch(pre.dataset.evidenceFile);if(!response.ok)throw Error('HTTP '+response.status);pre.textContent=await response.text();}pre.dataset.loaded='1'}catch(error){pre.textContent='Evidence could not be loaded: '+error.message;}}}
function initDiagnostic(){document.querySelectorAll('[data-call-log-open]').forEach(b=>b.addEventListener('click',()=>{openCallLog(b);loadDiagnosticEvidence(document.getElementById(b.dataset.callLogOpen))}));document.querySelectorAll('[data-call-log-close]').forEach(b=>b.addEventListener('click',()=>closeCallLog(b)));diagnosticAnchor()}
document.addEventListener('DOMContentLoaded',initDiagnostic);window.addEventListener('report-ready',initDiagnostic);window.addEventListener('hashchange',diagnosticAnchor);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

bug · medium
P2: Issue drill-down links do not apply their filters in the newly supported packaged diagnostics. The package loader converts analysis/index.html#issue=text into #page=analysis%2Findex.html&anchor=issue%3Dtext, but diagnosticAnchor() only recognizes a hash matching ^#issue=([a-z]+)$. Calling it on report-ready therefore does nothing, and the loader's applyReportAnchor() also has no issue handler. Clicking an output-defect link in the interpretation page shows the unfiltered inventory instead of the relevant failures. Decode the packaged anchor parameter (while retaining standalone hash support) and apply the issue filter after the virtual page is installed.

Comment thread src/harness/web_fetch/config.py Outdated
Comment on lines +132 to +134
if content_scope is not None:
from lexbench_webfetch.content_scopes import select_content_scope
raw_cases = [select_content_scope(item, content_scope)[0] for item in raw_cases]

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

bug · medium
P2: Apply scope selection even when there is no dataset-level or CLI override. A task can declare its own content_scope, and annotated tasks otherwise default to full in select_content_scope. This guard skips both cases, returning the original top-level requirements to execution and persistence, while score_attempt later replaces them with the selected profile. validate_case does not repair this: it discards the selector's returned dictionary. Consequently acquisition can use different requirements from scoring (for example, Cloudflare's video-link extraction reads case.expected_resources), and saved task requirements can misrepresent the scored contract. Run every task through select_content_scope(item, content_scope) before constructing EvalCase, retaining the selector's unchanged behavior for legacy tasks.

Comment thread src/harness/web_fetch/report.py Outdated
page('analysis/index.html',t('analysis', 'Analysis'),analysis)
guide=hero(t('extraction_results_for_these_pages', 'Extraction results for these pages'),(f"{str(model['metrics']['overall'][0])} / {str(len(rows))} " + t('first_calls_met_the_full_acceptance_requirements', 'first calls met the full acceptance requirements of the time; the diagnostics below cover the designated sample.')))
guide+=overall_section+section(t('results_and_measurement_scope', 'Results and measurement scope'),(f"{table([t('task_group', 'Task group'), t('implementation_version', 'Implementation version'), t('tasks', 'Tasks'), t('call_success', 'Call success'), t('pass', 'Pass'), t('mean_latency_s', 'Mean latency (s)'), t('p50_latency_s', 'P50 latency (s)'), t('p95_latency_s', 'P95 latency (s)')], summary_rows, 'guide-summary', sortable=True)}<p>" + t('latency_covers_all_calls_in_each_group', '{metric_note} Latency covers all calls in each group including failures; P50 is the median. Groups without complete timing are not computed.', metric_note=esc(model['metric_note'])) + '</p>'),'guide-results')
guide=hero(t('extraction_results_for_these_pages', 'Extraction results for these pages'),model.get('interpretation_lead') or (f"{str(model['metrics']['overall'][0])} / {str(len(rows))} " + t('first_calls_met_the_full_acceptance_requirements', 'first calls met the full acceptance requirements of the time; the diagnostics below cover the designated sample.')))

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

bug · medium
P2: Escape the new model-provided text before inserting it into HTML. hero() interpolates desc verbatim, and load_diagnostic() does not sanitize interpretation_lead; the new pass_label/fail_label interpolations below have the same problem. For example, a literal lead containing '

' becomes an element rather than visible text and fails the new get_text() equality check. HTML-bearing labels can also insert active markup into generated reports. Apply esc() to these model overrides, as already done for diagnostic_label, while preserving the existing trusted fallback markup.

Comment thread src/lexbench_webfetch/content_scopes.py Outdated
Comment on lines +88 to +89
if main.get("forbidden_any", []) != full.get("forbidden_any", []):
raise ValueError("Content scope cannot change blocked-page rejection rules")

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

bug · medium
P2: Compare these invariants against the original task as well, not just against the other profile. Both profiles can omit forbidden_any and source_benchmark, pass validation, and then have the defaults erase the task's blocked-page rules and navigation contract. For example, adding the test's fact-only profiles to a task with a forbidden marker permits an otherwise matching answer containing that marker to pass. The same omission removes the risk_control.navigation_contract used by score_attempt for page identity. Preserve these task-level protections or reject profiles that jointly remove/change them.

Comment on lines +102 to +104
effective = deepcopy(case)
effective.update(deepcopy(REQUIREMENT_DEFAULTS))
effective.update(deepcopy(profiles[selected]["requirements"]))

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

bug · medium
P2: The replacement leaves ordered_tree_reference and ordered_tree_min_similarity from the original case active, while the requirements allowlist prevents either profile from overriding them. A task with a full-page tree threshold and valid main-content requirements therefore still scores main output against the full-page tree; even a response satisfying every main requirement can fail ordered_tree_pass. Include tree acceptance in the scope replacement/validation contract (and its binding), or explicitly reject this combination rather than silently inheriting an unchangeable full-page requirement.

@lanyue-llk

Copy link
Copy Markdown
Contributor Author

Review of head 691cd74a: changes are needed before this can merge. The six open inline threads are all still unresolved at this head, and I haven't repeated them below.

CI (3 failures, all reproduced locally):

  • tests/benchmarks/test_current_scoring_controls.py: benchmark.py changed, but its hash in src/harness/benchmarks/code-migration.json was not refreshed.
  • tests/test_explicit_utf8_io.py: some new text reads and writes don't name encoding="utf-8".
  • tests/test_golden_reports.py::test_web_fetch_report: the golden output differs. Adding content_scopes.py to SCORING_SOURCE_FILES changes scoring_source_sha256, so the golden needs a deliberate refresh.

New findings:

  1. P1: rescore drops the scope the run used. See src/harness/web_fetch/cli.py:190 and config.py:131.
    • The run saves "content_scope": "main" in run/dataset.json, but rescore never reads it. Without --content-scope, score_attempt falls back to full, so verdicts silently flip from pass to fail.
    • Reproduced on the fixture dataset with main/full profiles, where full adds one footer fact:
      • run --content-scope main passes (main 1/1).
      • rescore run2 --dataset ds.json fails (full 1/2, content_pass=False) with no error.
    • The rescore guard at runner.py:1159 compares only url, track and task_kind.
    • Fix: rescore should use the saved scope by default, and reject an explicit scope that differs from it.
  2. P2: a diagnostic report can be packaged and published without its profile check. See src/harness/web_fetch/report_package.py:55-59.
    • validate_release stops early when evidence_state == "diagnostic" and calls check_diagnostic_report(source) without a profile. That skips require_diagnostic_scope and the diagnostic_input binding (report_diagnostic.py:467-472).
    • report_publish.publish and package can therefore publish a diagnostic whose cohort plan and input hash were never checked. AGENTS.md says incomplete cohorts are diagnostic only and must never be published.
    • The two places also detect a diagnostic differently: report-manifest.json evidence_state in one, report.json schema at line 145 in the other.
  3. P2: no paired control covers the new scoring path. See src/harness/benchmarks/scoring-regressions.json.
    • content_scopes.py joins SCORING_SOURCE_FILES, but none of the 7 new controls (main-product-*, reference-domain-*) sets content_scopes.
    • Add paired main/full controls that run through score_attempt with each scope.
  4. P3: public_task publishes everything in a profile's evidence. See src/lexbench_webfetch/benchmark.py:60-67.
    • It deep-copies the whole dict, but the validator only requires scope and source_sha256, so any extra key would be published.
    • The current HF data is safe: its only extra key is reference_versions, which holds hashes.
    • Fix: publish only the declared keys.

Version: this changes scoring (main/full selection), so per docs/releases.md it ships in a minor release (0.6.0) after 0.5.0.

Paired HF dataset PR (discussion #6): it pins tooling.revision and the README citation to 691cd74a, an unmerged commit of this PR, and names regression IDs that exist only on this branch. It needs re-pinning after this PR merges. Details are on the HF PR.

@lexmount-ocr lexmount-ocr Bot left a comment •

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Re-review complete · c75c771

This review found 8 issue(s), including 8 blocking issue(s).


function diagnosticAnchor(){let select=document.querySelector('select[data-filter="issue"]');if(!select)return;let match=location.hash.match(/^#issue=([a-z]+)$/);if(!match||![...select.options].some(o=>o.value===match[1]))return;document.querySelectorAll('select[data-filter]').forEach(s=>s.value=s.dataset.filter==='result'?'fail':'');select.value=match[1];cascadeFilters();document.getElementById('failure-inventory')?.scrollIntoView()}
document.addEventListener('DOMContentLoaded',()=>{document.querySelectorAll('[data-call-log-open]').forEach(b=>b.addEventListener('click',()=>{openCallLog(b)}));document.querySelectorAll('[data-call-log-close]').forEach(b=>b.addEventListener('click',()=>closeCallLog(b)));diagnosticAnchor()});window.addEventListener('hashchange',diagnosticAnchor); No newline at end of file
async function loadDiagnosticEvidence(dialog){for(let pre of dialog.querySelectorAll('pre[data-evidence-file]')){if(pre.dataset.loaded)continue;try{if(typeof textFile==='function'){pre.textContent=await textFile(pre.dataset.evidenceFile)}else{let response=await fetch(pre.dataset.evidenceFile);if(!response.ok)throw Error('HTTP '+response.status);pre.textContent=await response.text();}pre.dataset.loaded='1'}catch(error){pre.textContent='Evidence could not be loaded: '+error.message;}}}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

bug · medium
P2 — Reuse the existing evidence loader instead of redefining it. generate_cohort_diagnostic concatenates the shared report.js before this asset, and that file already defines loadDiagnosticEvidence and calls it inside openCallLog. This definition overrides the shared loader, dropping its in-flight guard and large-output textarea handling; the new click handler then calls it a second time. Opening any unloaded lazy-evidence dialog therefore starts two reads, and large evidence is inserted into a <pre> instead of using the existing bounded large-output path. Extend the shared loader for packaged textFile support if necessary and let openCallLog invoke it once.

Comment thread src/harness/web_fetch/config.py Outdated
Comment on lines +132 to +134
if content_scope is not None:
from lexbench_webfetch.content_scopes import select_content_scope
raw_cases = [select_content_scope(item, content_scope)[0] for item in raw_cases]

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

bug · medium
P2 — Apply selection for case-local scopes as well. With no CLI or dataset-level override, this branch is skipped even when a case declares content_scope='main' (or profiles defaulting to full). validate_case() calls the selector but discards its returned case, while score_attempt() does apply it. Consequently acquisition and scoring can use different requirements: for example, Exa's diagnostic request reads case.acceptance_contract to enable rich links, and Cloudflare reads case.expected_resources to request video links. Select each case unconditionally, passing the optional dataset override, before constructing EvalCase and saving the effective payload; the selector already preserves unannotated legacy cases.

headings=[h['text'] for h in reference.get('document_structure',{}).get('headings',[])]
except (KeyError,ValueError,TypeError):
headings=[]
add('pattern',t('document_headings_not_preserved','Document headings not preserved'),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

bug · medium
P2 — Do not equate every document-structure failure with missing headings. evaluate_document_structure also fails when document_body_chars is below document_minimum_body_chars, even when every heading passes and coverage is 100%. That valid scorer result now produces “Document headings not preserved” with “Heading coverage: 100%”, hiding the actual failed check. Distinguish heading failures from insufficient section-body content and display the saved body count and minimum when that predicate fails; add a control with complete headings but insufficient body text.

if (not rendered
or [c.get_text() for c in rendered.select('thead th')][1:] != spec['headers']
or [[c.get_text() for c in r.select('td')][1:] for r in rendered.select('tbody tr')]
!= [[diagnostic_cell_text(v) for v in r] for r in spec['rows']]):

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

bug · medium
P2 — Validate structured table-link destinations as well as their labels. Replacing an overview table's case link with another existing attempts/*.html route, changing its issue fragment, or removing the anchor while preserving its text passes this comparison. The later generic link check only verifies that a target file exists and ignores fragments, so it does not catch these mutations. This leaves the newly supported diagnosis indexes able to point at the wrong evidence while the release check succeeds. For each case_id/issue_id cell, require the corresponding anchor and exact expected href, and add negative controls that retain the label but change/remove the destination.

Comment on lines +671 to +672
if model.get('problem_focus'):
expected_examples = [example for block in model['pages'].get('guide', [])

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

bug · medium
P2 — Validate authored diagnoses whenever they are present, not only in problem-focused reports. validate_diagnostic_blocks accepts examples and figures without problem_focus, and generate_cohort_diagnostic.blocks() renders them in ordinary guides as well. For such a report, deleting an entire diagnosis article (or figure) leaves the legacy specimens intact and passes this release check. Run the model-to-rendered example/figure checks independently of this flag, keeping only the requirement for a nonempty diagnosis inventory conditional on problem_focus. Add a negative control that removes an authored diagnosis from a normal guide.

Comment on lines +880 to +881
if comparison_mode:
summary_rows=comparative_rows(summary_rows,{3:('delivery','max'),4:('acceptance','max')})

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

bug · medium
P2 — Compare success rates rather than raw counts, or require matched populations before enabling comparison mode. comparative_rows() uses each cell's data-sort, which here contains delivered/accepted counts, and diagnostic validation allows different cohort totals. Thus 8/10 is highlighted over 7/7 even though its acceptance rate is lower. Normalize the comparison metric by the cohort total while retaining the displayed numerator/denominator, or reject unequal populations; add an unequal-denominator control so winner highlighting cannot reverse the result.

Comment thread src/lexbench_webfetch/content_scopes.py Outdated
Comment on lines +88 to +89
if main.get("forbidden_any", []) != full.get("forbidden_any", []):
raise ValueError("Content scope cannot change blocked-page rejection rules")

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

bug · medium
P2 — Preserve the original case's invariant constraints, not just agreement between profiles. Both profiles can omit an existing forbidden_any list or supply the same altered source_benchmark identity and pass these checks. The subsequent defaults/update then removes the blocked-page rejection rules or replaces the source/navigation metadata used by scoring. This violates the selector's identity-preservation contract even though main and full agree with each other. Validate the invariant fields against the original case (or inherit them outside the replaceable acceptance surface), with regression controls where both profiles omit or identically alter an original constraint.

Comment on lines +102 to +104
effective = deepcopy(case)
effective.update(deepcopy(REQUIREMENT_DEFAULTS))
effective.update(deepcopy(profiles[selected]["requirements"]))

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

bug · medium
P2 — Include the ordered-tree acceptance fields in scope replacement. ordered_tree_reference and ordered_tree_min_similarity survive this update from the original case, but are also rejected as profile keys because they are absent from REQUIREMENT_DEFAULTS. Consequently, a task with a full-page tree threshold still evaluates that full-page tree after selecting main; score_attempt() applies the inherited threshold to content_pass (scoring.py:346–368), so an answer satisfying the independently annotated main requirements can fail on excluded content. Allow scope-specific tree contracts and clear inherited tree requirements when replacing the acceptance surface, or explicitly reject this combination rather than silently retaining the full-page check.

@lexmount-ocr lexmount-ocr Bot left a comment •

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Re-review complete · 312837e

This review found 5 issue(s), including 5 blocking issue(s).

Comment thread src/harness/web_fetch/cli.py Outdated
Comment on lines +193 to +194
if saved_scope is None and len(task_scopes)==1:
saved_scope=next(iter(task_scopes))

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

bug · medium
P2 — Do not promote a filtered population's task scope to a dataset-wide override. A source dataset without a top-level scope can validly contain an annotated task A (defaulting to full) and a historical unscoped task B. Running with --case-ids A saves only A, so this inference selects full on rescore. load_dataset then applies that override to every source task before the runner filters the population, and B raises Explicit content scope requires annotated main and full profiles. Thus an unchanged dataset cannot rescore this supported filtered run. Restore saved scopes per executed task rather than applying an inferred scope to unrelated source tasks, and add a mixed scoped/unscoped filtered-run regression.

coverage=metrics.get('document_heading_coverage')
if coverage is not None and coverage < contract.get('minimum_heading_coverage',1):
headings=[h['text'] for h in contract.get('headings',[])]
add('pattern',t('document_headings_not_preserved','Document headings not preserved'),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

bug · medium
P2 — Heading coverage is not a heading-presence metric: document_structure.py counts a heading as supported only when its required section body is long enough, and separately records document_heading_results[].present. An output containing every heading but undersized sections therefore gets reported here as “Document headings not preserved”; if total body length meets the global minimum, no body finding is added either. Use the per-heading present/pass results to distinguish absent headings from insufficient section text, as required by this change.

Comment on lines +425 to +426
path = contained_path(source, figure['path'], reject_symlinks=True)
raw = path.read_bytes()

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

bug · medium
P2 — Hash-bound figures cannot be built through the supported diagnostic-input-file path. build_diagnostic_cohorts() calls prepare_diagnostic_evidence(spec_path, staging) and then changes source to staging, but preparation only writes case evidence and report.json; it never copies spec['report']['pages'][...]['figure'] assets. This new read therefore raises FileNotFoundError for an illustrated input manifest even when its PNG exists alongside the input. Materialize and hash-check declared figures from the input root during preparation, and cover the file-input build path (not only preassembled report directories).

expected_examples = [example for block in page_blocks
for example in block.get('examples', [])]
rendered_examples = diagnosis_page.select('[data-diagnosis-case]')
if (model.get('problem_focus') and name=='guide' and not expected_examples) or len(expected_examples) != len(rendered_examples):

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

bug · medium
P2 — The concrete-case requirement can be bypassed by omitting pages.guide entirely. This condition runs only for existing pages entries, while problem_focus also disables the original specimen checks and replaces the rendered guide with blocks('guide'). A model with an explicit interpretation_lead and absent pages (or only an overview) can therefore pass this gate with no case diagnosis on its guide. Check for a nonempty guide example inventory outside this loop whenever problem_focus is enabled; add rejection coverage for both missing pages and missing guide, not just malformed examples.

Comment thread src/harness/web_fetch/report.py Outdated
guide+=section(t('conclusions_and_check_scope', 'Conclusions and check scope'),(f"<p>{esc(model['scope_note'])}</p><p>" + t('source_page_observations_and_output_checks_are', 'Source-page observations and output checks are kept separately. When an independent browser hits a verification page or access problem, this neither counts as a pass for the tested tool nor establishes that the tool is wrong. {link}', link=link('review/index.html', t('view_check_scope_and_handling', 'View check scope and handling →'))) + '</p>'),'guide-conclusions')
if model.get('problem_focus'):
core=section(t('results_and_measurement_scope','Results and measurement scope'),table([t('task_group','Task group'),t('implementation_version','Implementation version'),t('tasks','Tasks'),t('call_success','Call success'),t('pass','Pass'),t('mean_latency_s','Mean latency (s)'),t('p50_latency_s','P50 latency (s)'),t('p95_latency_s','P95 latency (s)')],summary_rows,'guide-summary')+'<p>'+esc(model['metric_note'])+'</p>','guide-results')
guide=hero(esc(model.get('guide_title')or model['title']),esc(model.get('interpretation_lead')or model['subtitle']))+core+blocks('guide')+section(t('conclusions_and_check_scope','Conclusions and check scope'),''.join('<p>'+esc(value)+'</p>'for value in model.get('guide_conclusions',[model['scope_note']])),'guide-conclusions')

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

bug · medium
P2 — The new problem-focused fallback disagrees with the release checker. With problem_focus=True and no interpretation_lead, this renders model['subtitle'], but check_diagnostic_report() still requires the lead to start with the numeric passed / total prefix. Neither model validation nor the renderer requires an explicit lead, so an otherwise valid model using this fallback fails build_diagnostic_cohorts() at its final check. Use the same numeric fallback as the ordinary guide (or define and validate one consistent alternative), and cover a problem-focused build without interpretation_lead.

@lexmount-ocr lexmount-ocr Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ Re-review complete · a59d90d

No findings in this incremental review.

@lanyue-llk

Copy link
Copy Markdown
Contributor Author

P0/P1 re-review of head a59d90d4: no P0 or P1 issues remain.

Checks:

  • Merge with main: the PR merges cleanly into current main f3c30da9 (v0.5.0), and tests/benchmarks/webfetch passes on the merged tree (11,155 passed).
  • Version label: after the merge, lexbench-webfetch version reports 0.5.0+unreleased with a new scorer hash (efb76ca2…). It doesn't claim to be the released 0.5.0, whose scorer hash is d0c5f12a….
  • Legacy scores are unchanged:
    • I scored 450 real v2.2.0 tasks (webfetch, articles, popular-sites, access-restrictions) with three synthetic answers each: full body, empty and half body.
    • On main and on the merged tree, all 1,350 results are identical apart from scoring_source_sha256.
    • dynamic-content wasn't covered, because its reference inputs aren't installed locally.
  • No reference-body leak: public profiles contain only the allowlisted evidence keys and hashed body bindings.

Earlier findings now fixed:

  • rescore reuses the run's saved scope and rejects a switch (cli.py:190-198, runner.py:1153-1157, with tests).
  • public_task publishes only an allowlist of evidence keys (benchmark.py:60-68).
  • Task-level and default scopes now apply when the dataset is loaded (config.py:131-133).
  • Profiles no longer erase forbidden_any or source_benchmark (content_scopes.py:89-112).
  • ordered_tree_* is rejected under the main scope (content_scopes.py:46-47).

Below P1, for context:

  • Annotated tasks default to the full profile, not to their top-level fields. Two tasks have top-level rules that differ from full.
  • benchmark_runner.py:68 has no --content-scope, so certified runs always use the default scope.
  • Because this changes the scorer hash, it ships in the next minor release (0.6.0).

Before merging: the paired dataset PR (HF #6) still has three P1 issues, listed on that PR. It needs rebuilding on dataset v2.2.0 and pinning to this PR's merged commit.

@lanyue-llk

Copy link
Copy Markdown
Contributor Author

The paired dataset PR has been updated: HF #6, head 048b9962c11ee8518374133203275c306bedb888.

  • Integrated dataset main c12559d2 (v2.2.0) into the PR branch and resolved the archive/manifest conflicts. The existing README, version history, license notices, task definitions, reference materials and 550-task edition metadata are retained.
  • Removed the duplicate data/content-scopes-530.jsonl viewer. The new 530-task cohort adds acceptance profiles to existing tasks; it does not increase the edition population.
  • Limited the historical revision review to its declared parent suites. The new annotation cohort no longer makes that test open a nonexistent materials directory.
  • Added two cohort/population tests. All 33 dataset tests and the publication check pass (task_count=550, archive_members=65). The manifest binds an uploaded source revision.

For the two nonblocking code observations: the certified runner calls load_dataset, so a frozen dataset-level or case-level content_scope already selects main/full before acquisition; it does not require a new CLI flag. Defaulting annotated tasks to their full profile is intentional, and acquisition and scoring now use the same selected requirements. The current code PR head remains a59d90d4, with no new review findings.

The annotation cohort explicitly identifies its unreleased dependency on this PR at a59d90d4. Its implementation pin still needs to move to the merged commit when the code is merged; no package/dataset release or PR merge was performed.

Server-side follow-ups, with Chinese reproductions and attached diagrams: setaria #95.

@lexmount-ocr lexmount-ocr Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ Re-review complete · 085f06b

No findings in this incremental review.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant