diff --git a/.agents/skills/benchmark-report/references/report-and-release.md b/.agents/skills/benchmark-report/references/report-and-release.md
index 56377ce48..eda24cf51 100644
--- a/.agents/skills/benchmark-report/references/report-and-release.md
+++ b/.agents/skills/benchmark-report/references/report-and-release.md
@@ -88,6 +88,7 @@ These rules apply to every public benchmark report, regardless of provider, task
- **Component denominators:** distinguish “a reference exists” from “that reference contains this component.” If a denominator is the union of reference-side and output-side content, show the counted categories, zero-score additions and pre-scoring exclusions with measured counts. Do not describe an output-only component as a missing reference.
- **Metric labels:** use parallel names and units (Mean latency, P50 latency, P95 latency). Explain their common population once beside the table. Define P50 as the median and P95 as the 95th percentile at first use; do not attach “successful” only to the mean while silently applying it to all three. Label unavailable values separately from zero.
- **Highlights:** derive unique directional extrema from raw numeric values and the complete displayed participant registry. Ties remain neutral. Green denotes a column extreme in the declared scope, not statistical significance or an overall ranking. Keep the same markers in all languages, themes, and copies of the comparison. Conditional success coverage and post-hoc shared-success subsets remain explicit.
+- **Table interaction:** choose controls by the table's reader task. Large result and dataset inventories must provide search or relevant filters plus stable sorting for decision-relevant columns. Compact comparison tables keep the complete participant set visible and provide accessible sorting for useful numeric columns; do not add filters that can hide participants and change the apparent comparison population. Small definition, provenance, or lookup tables may remain static. Sortable headers use buttons and `aria-sort`, cells expose raw machine-readable sort values where rendered text is formatted, unavailable values stay after real values in either direction, and ties retain source order. Sorting or filtering must never recompute directional highlights from the visible subset.
- **One fact, one owner:** use one result summary containing outcome and scalar facts. Distinguish request status, valid output, and task acceptance, but do not repeat them as several summary sections. Keep inventories compact and filterable. Put full output comparisons, scoring payloads, provenance, and logs behind labeled dialogs or disclosures; never flatten them into cards or inventory cells.
- **Repairable failures:** connect the failure location and program error category to exact error text, retained logs, relevant output or step, reviewed cause, and remediation evidence when recorded. Keep unknown fields explicitly unavailable; a warning or nearby successful action is not automatically the root cause. Preserve complete raw evidence, but do not duplicate a log stream in multiple visible sections.
- **Evidence interaction:** dialogs must have accessible names, close controls, Escape support, focus restoration, readable bounded scrolling, and usable narrow layouts. Verify a real failure from inventory to exact error/log and back, plus a successful output comparison. Test hosted navigation as well as local paths.
diff --git a/AGENTS.md b/AGENTS.md
index 641eac0f1..22fd9317f 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -21,8 +21,8 @@ These requirements apply to **all** report tracks; browser-specific rules below
- Run repository Python only through `make` targets or `uv run --frozen python`. `.python-version`, `uv.lock`, the Makefile and CI are the runtime contract; never fall back to system Python.
- Run every publishable provider cohort through `scripts/run_certified_benchmark.py`, which freezes references before calls and verifies them again afterward. Direct `run_benchmark.py` output and incomplete cohorts are diagnostic evidence only; never assemble or publish them.
- For the default Web Fetch report, treat `evaluation/current-report.json` as the matrix and publication-target authority. The independent WCXB report uses only `evaluation/wcxb/current-report.json` for its matrix and target. Browser Use and Browser Trajectory Replay have separate contracts under their own `evaluation/` namespaces; never reuse a publication target across reports or tracks. Publishing must use the relevant bound site and expected remote version; do not copy target IDs from prose, old receipts or earlier reports.
-- For the complete WebMainBench report, `evaluation/webmainbench-full/current-report.json` is the immutable five-method pre-call profile bound by its cohort certificate. The repaired Moli sixth version has its own pre-call profile and certificate at `evaluation/webmainbench-full/moli-fixed-report.json`; the report joins both complete 545-page cohorts by a hash-bound six-method receipt. Their `publication_state` records frozen pre-call state. The live site, share URL, and expected remote version are bound in `evaluation/webmainbench-full/publication-target.json` together with both profile hashes; update this target after publication without changing or re-certifying historical calls.
-- For the current WCXB report, the source-qualified 504-case manifest is the entire evaluation population for quality, delivery, and latency. Keep source-invalid cases outside the current manifest, scored evidence, public package, and future provider calls; preserve earlier physical calls only in private historical evidence. The dataset page owns frozen task inputs, source URLs and human reference scope; it must not show measured browser scores. The home page owns aggregate comparisons, the result analysis owns the filterable method-case inventory, and case result pages own full responses. Keep a direct link from each dataset row to a safe frozen-page preview and its human reference. Default result filters must show a nonempty, clearly labeled diagnostic population when one exists, and filtered tables must shrink rather than leave a large blank viewport.
+- For the complete WebMainBench report, `evaluation/webmainbench-full/current-report.json` is the immutable baseline pre-call profile bound by its cohort certificate. Independently certified variant cohorts are registered in `evaluation/webmainbench-full/`; discover the current variant registry and each variant's profile and certificate from that directory rather than hard-coding names. The method-binding file joins the baseline and all certified variant cohorts through locked profile hashes. Their `publication_state` records frozen pre-call state. The live site, share URL, and expected remote version are bound in `evaluation/webmainbench-full/publication-target.json` together with the baseline and all variant profile hashes; update this target after publication without changing or re-certifying historical calls.
+- For the current WCXB report, the source-qualified manifest is the entire evaluation population for quality, delivery, and latency. Keep source-invalid cases outside the current manifest, scored evidence, public package, and future provider calls; preserve earlier physical calls only in private historical evidence. The dataset page owns frozen task inputs, source URLs and human reference scope; it must not show measured browser scores. The home page owns aggregate comparisons, the result analysis owns the filterable method-case inventory, and case result pages own full responses. Keep a direct link from each dataset row to a safe frozen-page preview and its human reference. Default result filters must show a nonempty, clearly labeled diagnostic population when one exists, and filtered tables must shrink rather than leave a large blank viewport.
- Persist provider calls outside Git under `~/.browser-eval/` (or `BROWSER_EVAL_DATA_HOME`). Git may contain compact benchmark definitions, annotations, independent source evidence and public release metadata, but never measured outputs, execution manifests, provider artifacts, rerun directories, credentials or internal work logs.
- Web Fetch keeps one current authoritative logical result per method and task. A declared failure retry retains every physical call, retains total retry execution cost separately from the profile-defined primary latency and uses the final response, never the best response. A proven wrong command, version, scorer, reference or timing is a correction: preserve the prior evidence, supersede the complete declared scope and disclose score, denominator and latency effects.
- Aggregate only declared, hash-bound evidence files; reject symlinks and never publish worker directories, local paths or internal run trees. Every auxiliary and retry artifact named by a result must be materialized and hash-verified at the aggregate boundary.
@@ -39,7 +39,7 @@ These requirements apply to **all** report tracks; browser-specific rules below
- Maintain `research/report-changes.json` for substantive releases and `research/provider-pricing.json` for public pricing. Release notes explain reader-visible or result-visible effects without narrating assistant workflow; prices retain verification dates, commitments, billing units and unknowns.
- Render Web Fetch release history, including WebMainBench and the public benchmark research edition, from `research/report-changes.json`; keep Chinese and English release text together in each entry. Do not duplicate release prose or past measurements in report renderers or exact-string translation maps. Release checks compare structured records with rendered routes rather than matching fixed phrases. Browser Use and trajectory replay keep their existing structured release models.
- Use `Browser Use` for the autonomous track and `Browser Trajectory Replay` (Chinese: `轨迹重放`) for the fixed BrowserTool-action track. Their implementation roots are `harness/browser_use/` and `harness/trajectory_replay/`; do not add aliases or compatibility paths using the retired names.
-- The public WebArena human-recording replay is isolated under `harness/trajectory_replay/public_webarena/` with its own examples, tests, `evaluation/browser-use-public/current-report.json` and release script. It must not change autonomous `harness/browser_use/` or share a report target or denominator with the repository-curated 30-call replay suite.
+- The public WebArena human-recording replay is isolated under `harness/trajectory_replay/public_webarena/` with its own examples, tests, `evaluation/browser-use-public/current-report.json` and release script. It must not change autonomous `harness/browser_use/` or share a report target or denominator with the repository-curated replay suite.
- Browser Trajectory Replay canonical inputs must not contain run-local snapshot refs. Compile each ref to a hash-bound stable selector while preserving the original ref action semantics; do not silently replace a DOM ref click/fill/select with BrowserTool's Playwright selector fallback because that adds different viewport, overlay and navigation-wait behavior.
- Browser Trajectory Replay reports keep the dataset separate from measured verdicts. Every task needs a contract-and-qualified-trajectory page; every result needs a same-task all-browser comparison, a reader-facing failure locator, the exact failed-action or evaluator error, reviewed cause, complete expandable tool responses, and on-demand public evidence. The result inventory stays compact, fixed-height, filterable and linked to a diagnosis dialog. The same-task explorer uses a registry-driven baseline, multi-select comparison browsers and explicit outcome relations; group pass/fail links instead of adding one result column per future browser. Keep Chinese and English task/result routes aligned.
- A BrowserTool call that returns success does not prove its intended site mutation occurred. When the official evaluator rejects a completed trajectory for an absent required network event, locate the last plausible trigger action and publish the exact missing-event assertion, but do not mark that action as failed or change the official verdict. Repair asynchronous or position-dependent trajectory inputs only through new qualification evidence and a frozen full-scope comparison rerun.
diff --git a/evaluation/webmainbench-full/case-correction-binding.json b/evaluation/webmainbench-full/case-correction-binding.json
index 5f08b744f..58a7d6c8b 100644
--- a/evaluation/webmainbench-full/case-correction-binding.json
+++ b/evaluation/webmainbench-full/case-correction-binding.json
@@ -1,5 +1,5 @@
{
"schema_version": 1,
- "relative_path": "evidence/webmainbench-545-origin-correction-20260924",
- "sha256": "24ec49dba0948b85c0d980b3caf04b016f8fd486ceaa36eef43385b18cb4c9ab"
+ "relative_path": "evidence/webmainbench-545-origin-correction-seven-pr919-e944d066-rescored-20260929",
+ "sha256": "4bd72bf64c56f72566f7085cf6bdbb8107b1e34efe2b0f4845546e6fa0d9fc0e"
}
diff --git a/evaluation/webmainbench-full/content-binding.json b/evaluation/webmainbench-full/content-binding.json
index 44ce3853a..4f1e50514 100644
--- a/evaluation/webmainbench-full/content-binding.json
+++ b/evaluation/webmainbench-full/content-binding.json
@@ -1,5 +1,5 @@
{
"schema_version": 1,
- "relative_path": "report-content.json",
- "sha256": "c5094f07e653f323a2d4914b69a4533f510fe9e0cb9d7fb75041007ddae6e1cb"
+ "relative_path": "seven-method-moli-repairs/report-content.json",
+ "sha256": "92307a6be0ea8ec3bdae29045d19750fc4c66945dd901c9746eb8349647c054b"
}
diff --git a/evaluation/webmainbench-full/illustrations.json b/evaluation/webmainbench-full/illustrations.json
index cfa0e22d4..e71830564 100644
--- a/evaluation/webmainbench-full/illustrations.json
+++ b/evaluation/webmainbench-full/illustrations.json
@@ -2,16 +2,100 @@
"schema_version": 1,
"assets": [
{
- "name": "overview.png",
- "sha256": "1166017830d43cf56876e7cfc771d34dcd2b77998cac38d07bb84fa4e869d177",
+ "name": "seven-method-overview.png",
+ "role": "overview",
+ "sha256": "dde509a23ff5e0694df68c7b91974c388ddae3ff9441df6e161caddc6912f06e",
+ "generator": "deterministic HTML/CSS render",
+ "scope": "seven methods after certified origin, reference, TEDS and shared-population corrections",
+ "claims": {
+ "scores_percent": {
+ "moli": "36.41",
+ "lightpanda": "35.00",
+ "kitesurf": "33.43",
+ "chromium": "27.02",
+ "obscura": "31.03",
+ "moli-pr785-fixed": "36.53",
+ "moli-content-fixed": "39.41"
+ },
+ "machine_issues": 25,
+ "request_failures": 15,
+ "empty_outputs": 10,
+ "error_pages": 0
+ }
+ },
+ {
+ "name": "method-error-landscape.png",
+ "role": "quality-risk-landscape",
+ "sha256": "4b32244509a42764602dfbf16b13cb8d8f3351e5f7547b5489292698e124bbb5",
+ "generator": "deterministic HTML/CSS render",
+ "scope": "fixed-population quality thresholds and reviewed delivery failures for seven methods",
+ "claims": {
+ "denominators": {
+ "delivery": 545,
+ "text": 545,
+ "code": 110,
+ "formula": 257,
+ "table": 179,
+ "teds": 179
+ },
+ "reviewed_runtime_issues": 25
+ }
+ },
+ {
+ "name": "seven-method-error-taxonomy.png",
+ "role": "error-taxonomy",
+ "sha256": "a6d63aa9e38545f4b46d507ed7d8b7190cbb3e67879631b08e3f49d34e887c16",
+ "generator": "ImageGen",
+ "scope": "conceptual separation of delivery failures and content-quality gaps across seven methods"
+ },
+ {
+ "name": "moli-repair-mechanisms.png",
+ "role": "repair-mechanisms",
+ "sha256": "9db091bb41d693aa22862f2fe7e8f316ebfaaba1d2d1938fc515df915a8a329a",
+ "generator": "deterministic HTML/CSS render",
+ "scope": "observed defect to shared cause, implementation layer and verification"
+ },
+ {
+ "name": "moli-fix-01-visibility-disclosure.png",
+ "role": "moli-fix-type",
+ "sha256": "d7cb94763bdc1be0d8c479117e7d1e7eccb54d43392cf5d743a14e0e4615b8c1",
+ "generator": "ImageGen",
+ "scope": "visibility, disclosure content and hidden-subtree boundaries"
+ },
+ {
+ "name": "moli-fix-02-fallback-fragments.png",
+ "role": "moli-fix-type",
+ "sha256": "7d1dfec266bd0f6a070fc47e918806dce8fc6bf27d6cd32fbaffa3a3b12f1568",
+ "generator": "ImageGen",
+ "scope": "fallback conversion, escaping and fragment aliases"
+ },
+ {
+ "name": "moli-fix-03-live-form-state.png",
+ "role": "moli-fix-type",
+ "sha256": "852e50109f160588f6df5fe34ac66a39cfa81f341fb6a53b13b40360bbf3a571",
+ "generator": "ImageGen",
+ "scope": "live form state instead of serialized initial attributes"
+ },
+ {
+ "name": "moli-fix-04-media-candidates.png",
+ "role": "moli-fix-type",
+ "sha256": "5d89b58c2ba3618ef0ef04ecd02cba7f57e837c36ee11ea2594c433cec0dbbc4",
+ "generator": "ImageGen",
+ "scope": "standard and lazy media candidate precedence with URL safety"
+ },
+ {
+ "name": "moli-fix-05-semantic-boundaries.png",
+ "role": "moli-fix-type",
+ "sha256": "eadaf97a6703c5649674f75986a200701159ff4c882a48898c2c3a539ebfb6ce",
"generator": "ImageGen",
- "scope": "six methods after certified origin correction"
+ "scope": "math, code and table semantic boundaries"
},
{
- "name": "error-mechanisms.png",
- "sha256": "c53b86571b5c20976f9a54ccfe33d065111732399fceeb4a148c74c520d3d843",
+ "name": "moli-fix-06-parser-resume-ownership.png",
+ "role": "moli-fix-type",
+ "sha256": "720259b73c1cbc7d0eccecc4cc8618cefa09c20902bb5d2fd34500c47716ad70",
"generator": "ImageGen",
- "scope": "six methods after certified origin correction"
+ "scope": "parser suspension resume ownership"
}
]
}
diff --git a/evaluation/webmainbench-full/moli-content-fixed-report.json b/evaluation/webmainbench-full/moli-content-fixed-report.json
new file mode 100644
index 000000000..86806d171
--- /dev/null
+++ b/evaluation/webmainbench-full/moli-content-fixed-report.json
@@ -0,0 +1,28 @@
+{
+ "schema_version": 1,
+ "dataset_id": "webmainbench-545-moli-fixed",
+ "comparison_kind": "webmainbench_frozen_html_to_markdown",
+ "case_count": 545,
+ "case_ids_sha256": "5c0cf5810602b70e8f7c1e16723e24a49c3ea207d54865df5c03c38f3d88ffd5",
+ "source_dataset_sha256": "03bc6370a2b37427eba94f95268cf3c935ac89b15b184dfc47f84b78b97fe18e",
+ "providers": ["moli"],
+ "versions": {"moli": "1.1.11"},
+ "binary_sha256": "e146b853bb06ea02bb8847407a76e28eb2bc31ba4ce741af3a7fdaabb194e209",
+ "source_commit": "e944d066bc12493bc05e737f07c5721d5be32cf6",
+ "timeout_seconds": 45,
+ "runs_per_case": 1,
+ "concurrency": 1,
+ "max_failure_retries": 1,
+ "reference_binding_mode": "complete_webmainbench_545_official_frozen_html_and_groundtruth_content",
+ "official_scorer_commit": "f7cb97df2dfb5ac10ebc97cd0b8f9d73ab40f50b",
+ "metric_config": {"use_llm": false},
+ "official_metrics": ["overall", "text_edit", "code_edit", "formula_edit", "table_edit", "table_TEDS"],
+ "input_contract": {
+ "body": "official html field encoded as UTF-8 without mutation",
+ "execution": "inline scripts may run; non-loopback subresources and data access are blocked; local engine receives the same body from an OS-isolated loopback HTTPS fixture with a per-run CA",
+ "original_url": "retained as evidence metadata only"
+ },
+ "publication_state": "pending",
+ "require_independent_result_review": false,
+ "conversion": "Native Markdown from Moli PR #919 after shared fallback semantics, live form state, proven interactive disclosures, bounded one-shot fade-in final-state recovery, media candidate and fragment-target fixes, based on main cb56b3b33"
+}
diff --git a/evaluation/webmainbench-full/moli-fixed-report.json b/evaluation/webmainbench-full/moli-pr785-fixed-report.json
similarity index 100%
rename from evaluation/webmainbench-full/moli-fixed-report.json
rename to evaluation/webmainbench-full/moli-pr785-fixed-report.json
diff --git a/evaluation/webmainbench-full/moli-variants.json b/evaluation/webmainbench-full/moli-variants.json
new file mode 100644
index 000000000..a13d7c6ed
--- /dev/null
+++ b/evaluation/webmainbench-full/moli-variants.json
@@ -0,0 +1,23 @@
+{
+ "schema_version": 1,
+ "variants": [
+ {
+ "provider": "moli-pr785-fixed",
+ "profile": "moli-pr785-fixed-report.json",
+ "evidence_alias": "moli-fixed",
+ "label": "Moli 1.1.9 · PR785 修复版",
+ "short_label": "PR785 修复版",
+ "role": "prior_repair",
+ "pull_request": "https://github.com/lexmount/moli/pull/785"
+ },
+ {
+ "provider": "moli-content-fixed",
+ "profile": "moli-content-fixed-report.json",
+ "evidence_alias": "moli-fixed",
+ "label": "Moli 1.1.11 · PR919 修复版",
+ "short_label": "PR919 修复版",
+ "role": "content_repair",
+ "pull_request": "https://github.com/lexmount/moli/pull/919"
+ }
+ ]
+}
diff --git a/evaluation/webmainbench-full/publication-target.json b/evaluation/webmainbench-full/publication-target.json
index 5ef2febea..e8737ceb2 100644
--- a/evaluation/webmainbench-full/publication-target.json
+++ b/evaluation/webmainbench-full/publication-target.json
@@ -1,9 +1,12 @@
{
"schema_version": 1,
"profile_sha256": "8cbceaf9694e88e7c1ddbba3263573dcccf69b1ac89d7e9ea5aa098f2a3bab02",
- "moli_fixed_profile_sha256": "db8e7fa408edf3dc2e19d2b0f35645448d0e1efd3ac86926d0bcc9e1a3fb3848",
+ "variant_profile_sha256": {
+ "moli-pr785-fixed": "db8e7fa408edf3dc2e19d2b0f35645448d0e1efd3ac86926d0bcc9e1a3fb3848",
+ "moli-content-fixed": "e3a4b3abd03e29ef3aab13e27e11b614d06f0d822fad86b08c61dbde1501b6bf"
+ },
"host": "https://artifact-site.lexmount.cn",
"site_slug": "zrqPjO672dgg",
"share_url": "https://artifact-site.lexmount.cn/v/Avz3qzo6EEujI0iln1LAzCUF93I47OEC",
- "expected_remote_version": "ver_8cbf0496af25497283fdcb750756b422"
+ "expected_remote_version": "ver_9440fb2764424af387e7a3c41cf6720f"
}
diff --git a/evaluation/webmainbench-full/scoring-correction-binding.json b/evaluation/webmainbench-full/scoring-correction-binding.json
new file mode 100644
index 000000000..786e43d8d
--- /dev/null
+++ b/evaluation/webmainbench-full/scoring-correction-binding.json
@@ -0,0 +1,8 @@
+{
+ "schema_version": 1,
+ "kind": "webmainbench_shared_population_scoring_correction_binding",
+ "receipt_sha256": "514c99c3c8b55b29eccc38fd87efecc40ebf8602f2e9e84b6807538df696061e",
+ "scorer_commit": "f7cb97df2dfb5ac10ebc97cd0b8f9d73ab40f50b",
+ "regression_registry_sha256": "ddd89c55e5791a94f3d9282dc3a56fbefed2cb72aad8b0fc43d5275a3bca7cd3",
+ "results_sha256": "08f4464fe98c57910bcc21b63d3dad8c87d3bd505c8f181c89f169257fd8f439"
+}
diff --git a/evaluation/webmainbench-full/scoring-regressions.json b/evaluation/webmainbench-full/scoring-regressions.json
new file mode 100644
index 000000000..2e97d475a
--- /dev/null
+++ b/evaluation/webmainbench-full/scoring-regressions.json
@@ -0,0 +1,43 @@
+{
+ "schema_version": 1,
+ "cases": [
+ {
+ "id": "prediction-only-code-does-not-expand-population",
+ "root_cause": "method_defined_specialized_denominator",
+ "failure_mode": "false-positive",
+ "predicted": "use `invented()`",
+ "reference": "plain reference",
+ "meta": {"code": []},
+ "expected_success": false
+ },
+ {
+ "id": "missing-reference-scoped-code-scores-zero",
+ "root_cause": "method_defined_specialized_denominator",
+ "failure_mode": "false-negative",
+ "predicted": "plain prediction",
+ "reference": "use `expected()`",
+ "meta": {"code": ["inline"]},
+ "expected_success": true,
+ "expected_score": 0.0
+ },
+ {
+ "id": "raw-html-pre-is-scorable-code",
+ "root_cause": "code_representation_coverage",
+ "failure_mode": "false-negative",
+ "predicted": "
one()\ntwo()
",
+ "reference": "```\none()\ntwo()\n```",
+ "meta": {"code": ["interline"]},
+ "expected_success": true,
+ "expected_score": 1.0
+ },
+ {
+ "id": "layout-indented-prose-is-not-code",
+ "root_cause": "code_representation_coverage",
+ "failure_mode": "false-positive",
+ "predicted": "plain",
+ "reference": "Abstract\n\n wrapped article prose on one line\n and another prose line.\n",
+ "meta": {"code": []},
+ "expected_success": false
+ }
+ ]
+}
diff --git a/harness/web_fetch/report.py b/harness/web_fetch/report.py
index 104654d00..c64120a0b 100644
--- a/harness/web_fetch/report.py
+++ b/harness/web_fetch/report.py
@@ -2922,6 +2922,8 @@ def prose(key):
return esc(content[key])
correction_path = out / 'case-correction.json'
correction = json.loads(correction_path.read_text()) if correction_path.exists() else None
+ scoring_correction_path = out / 'scoring-correction.json'
+ scoring_correction = json.loads(scoring_correction_path.read_text()) if scoring_correction_path.exists() else None
def illustration(name, caption):
if not (out / 'assets' / name).is_file():
return ''
@@ -2929,12 +2931,31 @@ def illustration(name, caption):
'">' + esc(caption) + '(窄屏可横向滚动,点击查看原图。)')
providers = profile["providers"]
+ method_error_profiles = content.get("method_error_profiles")
+ if (not isinstance(method_error_profiles, dict)
+ or set(method_error_profiles) != set(providers)
+ or any(set(item) != {"runtime", "quality", "reading"}
+ for item in method_error_profiles.values())):
+ raise ValueError("Report content lacks a complete seven-method error profile")
+ moli_fix_profiles = content.get("moli_fix_profiles")
+ expected_moli_fix_images = [f"moli-fix-{index:02d}-{suffix}.png" for index, suffix in (
+ (1, "visibility-disclosure"), (2, "fallback-fragments"),
+ (3, "live-form-state"), (4, "media-candidates"),
+ (5, "semantic-boundaries"), (6, "parser-resume-ownership"))]
+ if (not isinstance(moli_fix_profiles, list)
+ or [item.get("image") for item in moli_fix_profiles] != expected_moli_fix_images
+ or any(set(item) != {"image", "title", "symptom", "root", "fix", "verification"}
+ for item in moli_fix_profiles)):
+ raise ValueError("Report content lacks the six bound Moli fix profiles")
cases = {case["id"]: case for case in dataset["cases"]}
grouped = defaultdict(dict)
for row in records:
grouped[row["case_id"]][row["provider"]] = row
- labels = {p: ("Moli 1.1.9(修复后)" if p == "moli-fixed" else profile["versions"][p] if p == "chromium" else p.capitalize() +
+ variant_labels = profile.get("variant_labels", {})
+ labels = {p: (variant_labels[p] if p in variant_labels else profile["versions"][p] if p == "chromium" else p.capitalize() +
(" " + profile["versions"][p] if p != "kitesurf" else "")) for p in providers}
+ content_fixed_commit = json.loads((out / 'moli-content-fixed-report.json').read_text(encoding='utf-8'))['source_commit']
+ content_fixed_version = profile['versions']['moli-content-fixed']
measures = [("overall", "综合分"), ("text_edit", "全文"), ("code_edit", "代码"),
("formula_edit", "公式"), ("table_edit", "表格内容"), ("table_TEDS", "表格结构")]
@@ -2965,11 +2986,12 @@ def classify(row):
def target_body_count(provider):
return sum(target_body[row["attempt_id"]] for row in records if row["provider"] == provider)
css = """*{box-sizing:border-box}body{margin:0;background:#f4f7fb;color:#172637;font:16px/1.55 system-ui,sans-serif}body.dark{background:#101923;color:#edf3fa}a{color:#1859a5}body.dark a{color:#92c7ff}.wrap{width:min(1200px,calc(100% - 32px));margin:auto}header{background:#142d4b;color:white}header .wrap{display:flex;align-items:center;gap:18px;flex-wrap:wrap;padding:15px 0}header a{color:white;text-decoration:none}header strong{margin-right:auto}nav{display:flex;gap:14px;flex-wrap:wrap}button,input,select{font:inherit;border:1px solid #9aacc0;border-radius:7px;padding:7px 10px;background:white;color:#172637}body.dark button,body.dark input,body.dark select{background:#25384b;color:white;border-color:#61768b}h1{font-size:clamp(30px,4vw,48px);line-height:1.18;margin:0 0 15px}h2{font-size:26px;margin:0 0 14px}.hero{padding:48px 0 22px}.hero p{font-size:18px;max-width:900px;color:#506377}body.dark .hero p,body.dark .note{color:#bfd0e1}section{margin:24px 0 38px}figure{margin:24px 0}.panel{background:white;border:1px solid #d8e1eb;border-radius:12px;padding:20px;margin:14px 0}body.dark .panel{background:#1c2b3b;border-color:#38506a}.scroll{overflow:auto;max-height:66vh;border:1px solid #d8e1eb;border-radius:10px;background:white}body.dark .scroll{background:#1c2b3b;border-color:#38506a}table{border-collapse:collapse;width:100%;min-width:820px}th,td{padding:9px 12px;text-align:left;border-bottom:1px solid #dce5ee;vertical-align:top}th{position:sticky;top:0;background:#e9f0f8;color:#152a40;z-index:1}body.dark th{background:#29435d;color:white}tbody tr:hover{background:#edf4fc}body.dark tbody tr:hover{background:#28445f}td small{display:block;color:#597088}body.dark td small{color:#b8cadb}.number{font-variant-numeric:tabular-nums;white-space:nowrap}.winner{color:#116b3c;font-weight:750}body.dark .winner{color:#82e1a7}.filters{display:flex;gap:12px;flex-wrap:wrap;margin:14px 0}.filters label{display:grid;gap:4px;font-size:13px;font-weight:700}.note{color:#52677b;font-size:14px}pre{white-space:pre-wrap;overflow-wrap:anywhere;background:#0e2032;color:#f1f6fb;padding:17px;border-radius:8px;max-height:65vh;overflow:auto;font:13px/1.5 ui-monospace,monospace}.compare-grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:16px}.compare-grid .panel{min-width:0;margin:0}.compare-grid pre{max-height:70vh}details{border:1px solid #dce5ee;border-radius:8px;padding:10px 14px;margin:10px 0}body.dark details{border-color:#38506a}summary{cursor:pointer;font-weight:700}.actions{display:flex;gap:14px;flex-wrap:wrap}.source{overflow-wrap:anywhere}footer{padding:32px 0;color:#52677b}@media(max-width:800px){.compare-grid{grid-template-columns:1fr}}@media(max-width:650px){.panel{padding:14px}header nav{width:100%}th,td{padding:7px 9px}}"""
- js = """function theme(){document.body.classList.toggle('dark');try{localStorage.setItem('wmb545-theme',document.body.classList.contains('dark')?'dark':'light')}catch(e){}}try{if(localStorage.getItem('wmb545-theme')==='dark')document.body.classList.add('dark')}catch(e){}function filter(){let q=(document.getElementById('q')?.value||'').toLowerCase(),p=document.getElementById('provider')?.value||'',k=document.getElementById('kind')?.value||'issue',cause=document.getElementById('cause')?.value||'',n=0;document.querySelectorAll('tr[data-result]').forEach(r=>{let show=(!q||r.dataset.search.includes(q))&&(!p||r.dataset.provider===p)&&(!cause||r.dataset.cause===cause)&&(k==='all'||(k==='issue'?r.dataset.kind!=='未触发诊断':r.dataset.kind===k));r.hidden=!show;if(show)n++});let c=document.getElementById('count');if(c)c.textContent=n;let e=document.getElementById('empty');if(e)e.hidden=n!==0}document.addEventListener('DOMContentLoaded',()=>{for(let id of ['q','provider','kind','cause'])document.getElementById(id)?.addEventListener('input',filter);let params=new URLSearchParams(location.search);for(let id of ['provider','kind','cause']){let select=document.getElementById(id);if(select&¶ms.has(id))select.value=params.get(id)}filter();let node=document.getElementById('evidence-content');if(node){let path=params.get('path')||'';if(!/^(artifacts|references|sources)\\/[A-Za-z0-9_.\\/-]+$/.test(path)||path.includes('..')){node.textContent='无效证据路径';return}document.getElementById('evidence-path').textContent=path;document.getElementById('evidence-raw').href=path;loadEvidence(path,node)}});"""
+ js = """function theme(){document.body.classList.toggle('dark');try{localStorage.setItem('wmb545-theme',document.body.classList.contains('dark')?'dark':'light')}catch(e){}}try{if(localStorage.getItem('wmb545-theme')==='dark')document.body.classList.add('dark')}catch(e){}function filter(){let q=(document.getElementById('q')?.value||'').toLowerCase(),p=document.getElementById('provider')?.value||'',k=document.getElementById('kind')?.value||'issue',cause=document.getElementById('cause')?.value||'',quality=document.getElementById('metric')?.value||'',n=0;document.querySelectorAll('tr[data-result]').forEach(r=>{let flags=(r.dataset.quality||'').split(' '),show=(!q||r.dataset.search.includes(q))&&(!p||r.dataset.provider===p)&&(!cause||r.dataset.cause===cause)&&(!quality||flags.includes(quality))&&(k==='all'||(k==='issue'?r.dataset.kind!=='未触发诊断':r.dataset.kind===k));r.hidden=!show;if(show)n++});let c=document.getElementById('count');if(c)c.textContent=n;let e=document.getElementById('empty');if(e)e.hidden=n!==0}function filterDataset(){let q=(document.getElementById('dataset-q')?.value||'').toLowerCase(),language=document.getElementById('dataset-language')?.value||'',level=document.getElementById('dataset-level')?.value||'',n=0;document.querySelectorAll('tr[data-dataset]').forEach(r=>{let show=(!q||r.dataset.search.includes(q))&&(!language||r.dataset.language===language)&&(!level||r.dataset.level===level);r.hidden=!show;if(show)n++});let count=document.getElementById('dataset-count');if(count)count.textContent=n;let empty=document.getElementById('dataset-empty');if(empty)empty.hidden=n!==0}function sortReportTable(button){let table=button.closest('table'),head=button.closest('th'),column=[...head.parentElement.cells].indexOf(head),type=button.dataset.sortType||'text',direction=head.getAttribute('aria-sort')==='ascending'?'descending':'ascending',body=table.tBodies[0],rows=[...body.rows].map((row,index)=>({row,index}));let valueOf=cell=>{let raw=cell?.dataset.sort??cell?.textContent.trim()??'';if(raw===''||raw==='—')return{missing:true,value:null};if(type==='number'){let number=Number(String(raw).replace(/[,%%]/g,''));return{missing:!Number.isFinite(number),value:number}}return{missing:false,value:String(raw).trim()}};rows.sort((left,right)=>{let a=valueOf(left.row.cells[column]),b=valueOf(right.row.cells[column]);if(a.missing!==b.missing)return a.missing?1:-1;if(a.missing&&b.missing)return left.index-right.index;let compared=type==='number'?a.value-b.value:a.value.localeCompare(b.value,'zh-CN',{numeric:true,sensitivity:'base'});return compared===0?left.index-right.index:(direction==='ascending'?compared:-compared)});body.replaceChildren(...rows.map(item=>item.row));table.querySelectorAll('th[aria-sort]').forEach(item=>item.setAttribute('aria-sort','none'));head.setAttribute('aria-sort',direction)}document.addEventListener('DOMContentLoaded',()=>{for(let id of ['q','provider','kind','cause','metric'])document.getElementById(id)?.addEventListener('input',filter);for(let id of ['dataset-q','dataset-language','dataset-level'])document.getElementById(id)?.addEventListener('input',filterDataset);let params=new URLSearchParams(location.search);for(let id of ['provider','kind','cause']){let select=document.getElementById(id);if(select&¶ms.has(id))select.value=params.get(id)}filter();filterDataset();let node=document.getElementById('evidence-content');if(node){let path=params.get('path')||'';if(!/^(artifacts|references|sources)\\/[A-Za-z0-9_.\\/-]+$/.test(path)||path.includes('..')){node.textContent='无效证据路径';return}document.getElementById('evidence-path').textContent=path;document.getElementById('evidence-raw').href=path;loadEvidence(path,node)}});"""
+ js = js.replace("for(let id of ['provider','kind','cause'])", "for(let id of ['provider','kind','cause','metric'])")
js += """
async function loadJson(path){let response=await fetch(path);if(!response.ok)throw Error('HTTP '+response.status);return response.json()}
async function loadEvidence(path,node){try{let index=await loadJson('artifact-shard-index.json'),shardPath=index[path];if(!shardPath)throw Error('证据索引缺失');let shard=await loadJson(shardPath),hash=shard.artifacts[path],value=shard.blobs[hash];if(typeof value!=='string')throw Error('证据内容缺失');node.textContent=value;let raw=document.getElementById('evidence-raw');raw.href=URL.createObjectURL(new Blob([value],{type:'text/plain;charset=utf-8'}));raw.download=path.split('/').pop()}catch(error){node.textContent='无法读取证据:'+error.message}}
-async function renderResult(){let params=new URLSearchParams(location.search),provider=params.get('provider')||'',id=params.get('id')||'';let title=document.getElementById('result-title');if(!/^[a-z0-9-]+$/.test(provider)||!/^[A-Za-z0-9-]+$/.test(id)){title.textContent='无效结果地址';return}try{let index=await loadJson('result-shard-index.json'),key=provider+'|'+id,shardPath=index[key];if(!shardPath)throw Error('结果索引缺失');let shard=await loadJson(shardPath),row=shard.results[key];if(!row)throw Error('结果内容缺失');let names={moli:'Moli 1.1.9', 'moli-fixed':'Moli 1.1.9(修复后)',lightpanda:'Lightpanda',obscura:'Obscura',chromium:'Chrome',kitesurf:'Kitesurf'},name=names[provider]||provider;title.textContent=name+' · '+id;document.title=title.textContent+' · WebMainBench 545';document.getElementById('result-source').textContent=row.original_url;let failure=['请求失败','空输出','错误页'].includes(row.diagnosis),detail=document.querySelector('[data-report-detail]');detail.dataset.reportDetail=failure?'failed':'success';document.getElementById('result-summary').textContent='状态:'+(row.ok?'返回成功':'请求失败')+';诊断:'+row.diagnosis+';最终调用 '+row.elapsed_ms.toFixed(0)+' ms;物理调用 '+row.physical_attempt_count+' 次,含重试总耗时 '+row.total_elapsed_ms.toFixed(0)+' ms。';let correction=document.getElementById('result-correction');correction.hidden=!row.supersedes_attempt_id;let section=document.getElementById('result-failure');section.hidden=!failure;if(failure){document.getElementById('result-stage').textContent='阶段:'+(row.error_type||row.diagnosis);document.getElementById('result-error').textContent='程序错误:'+(row.error_message||'无');document.getElementById('result-log').textContent=row.provider_metadata?.stderr||row.provider_metadata?.browser_stderr||'无日志';document.getElementById('result-cause').textContent='复核原因:'+(row.review?.cause||'未记录');document.getElementById('result-treatment').textContent='处理:'+(row.review?.treatment||'未记录').replace(/[。;]+$/,'')+';状态:'+(row.review?.status||'未记录')}let labels=[['text_edit','全文'],['code_edit','代码'],['formula_edit','公式'],['table_edit','表格内容'],['table_TEDS','表格结构']],body=document.getElementById('result-metrics');for(let [metric,label] of labels){let tr=document.createElement('tr'),entry=row.metrics?.[metric]||{};for(let value of [label,(!entry.success||entry.score==null)?'—':(entry.score*100).toFixed(1)+'%',entry.success?'参与计分':'不参与计分']){let td=document.createElement('td');td.textContent=value;tr.append(td)}body.append(tr)}document.getElementById('result-case').href='cases/index.html?id='+id;document.getElementById('result-output').href='viewer/index.html?path='+encodeURIComponent(row.artifact_path);document.getElementById('result-reference').href='viewer/index.html?path=references/'+id+'.md';document.getElementById('result-source-link').href='viewer/index.html?path=sources/'+id+'.html.txt';let physical=shard.physical[id]||[],list=document.getElementById('result-physical-list');for(let [number,attempt] of physical.entries()){let li=document.createElement('li'),a=document.createElement('a');a.href='viewer/index.html?path='+encodeURIComponent(attempt.physical_artifact_path);a.textContent='第 '+(number+1)+' 次调用 · '+attempt.elapsed_ms.toFixed(0)+' ms';li.append(a);list.append(li)}document.getElementById('result-physical').hidden=!physical.length}catch(error){title.textContent='无法载入结果:'+error.message}}
+async function renderResult(){let params=new URLSearchParams(location.search),provider=params.get('provider')||'',id=params.get('id')||'';let title=document.getElementById('result-title');if(!/^[a-z0-9-]+$/.test(provider)||!/^[A-Za-z0-9-]+$/.test(id)){title.textContent='无效结果地址';return}try{let index=await loadJson('result-shard-index.json'),key=provider+'|'+id,shardPath=index[key];if(!shardPath)throw Error('结果索引缺失');let shard=await loadJson(shardPath),row=shard.results[key];if(!row)throw Error('结果内容缺失');let names=__PROVIDER_LABELS_JSON__,name=names[provider]||provider;title.textContent=name+' · '+id;document.title=title.textContent+' · WebMainBench 545';document.getElementById('result-source').textContent=row.original_url;let failure=['请求失败','空输出','错误页'].includes(row.diagnosis),detail=document.querySelector('[data-report-detail]');detail.dataset.reportDetail=failure?'failed':'success';document.getElementById('result-summary').textContent='状态:'+(row.ok?'返回成功':'请求失败')+';诊断:'+row.diagnosis+';最终调用 '+row.elapsed_ms.toFixed(0)+' ms;物理调用 '+row.physical_attempt_count+' 次,含重试总耗时 '+row.total_elapsed_ms.toFixed(0)+' ms。';let correction=document.getElementById('result-correction');correction.hidden=!row.supersedes_attempt_id;let section=document.getElementById('result-failure');section.hidden=!failure;if(failure){document.getElementById('result-stage').textContent='阶段:'+(row.error_type||row.diagnosis);document.getElementById('result-error').textContent='程序错误:'+(row.error_message||'无');document.getElementById('result-log').textContent=row.provider_metadata?.stderr||row.provider_metadata?.browser_stderr||'无日志';document.getElementById('result-cause').textContent='复核原因:'+(row.review?.cause||'未记录');document.getElementById('result-treatment').textContent='处理:'+(row.review?.treatment||'未记录').replace(/[。;]+$/,'')+';状态:'+(row.review?.status||'未记录')}let labels=[['text_edit','全文'],['code_edit','代码'],['formula_edit','公式'],['table_edit','表格内容'],['table_TEDS','表格结构']],body=document.getElementById('result-metrics');for(let [metric,label] of labels){let tr=document.createElement('tr'),entry=row.metrics?.[metric]||{};for(let value of [label,(!entry.success||entry.score==null)?'—':(entry.score*100).toFixed(1)+'%',entry.success?'参与计分':'不参与计分']){let td=document.createElement('td');td.textContent=value;tr.append(td)}body.append(tr)}document.getElementById('result-case').href='cases/index.html?id='+id;document.getElementById('result-output').href='viewer/index.html?path='+encodeURIComponent(row.artifact_path);document.getElementById('result-reference').href='viewer/index.html?path=references/'+id+'.md';document.getElementById('result-source-link').href='viewer/index.html?path=sources/'+id+'.html.txt';let physical=shard.physical[id]||[],list=document.getElementById('result-physical-list');for(let [number,attempt] of physical.entries()){let li=document.createElement('li'),a=document.createElement('a');a.href='viewer/index.html?path='+encodeURIComponent(attempt.physical_artifact_path);a.textContent='第 '+(number+1)+' 次调用 · '+attempt.elapsed_ms.toFixed(0)+' ms';li.append(a);list.append(li)}document.getElementById('result-physical').hidden=!physical.length}catch(error){title.textContent='无法载入结果:'+error.message}}
async function renderCase(){
let id=new URLSearchParams(location.search).get('id')||'',slot=document.getElementById('case-id');
if(!/^[A-Za-z0-9-]+$/.test(id)){slot.textContent='无效任务地址';return}
@@ -2981,7 +3003,7 @@ def target_body_count(provider):
document.getElementById('case-source').href='viewer/index.html?path=sources/'+id+'.html.txt';
document.getElementById('case-reference').href='viewer/index.html?path=references/'+id+'.md';
let resultIndex=await loadJson('result-shard-index.json'),artifactIndex=await loadJson('artifact-shard-index.json');
- let names={moli:'Moli 1.1.9','moli-fixed':'Moli 1.1.9(修复后)',lightpanda:'Lightpanda',obscura:'Obscura',chromium:'Chrome',kitesurf:'Kitesurf'};
+ let names=__PROVIDER_LABELS_JSON__;
let metrics=['overall','text_edit','code_edit','formula_edit','table_edit','table_TEDS'];
let providers=dataset.coverage_plan.static_html,referencePath='references/'+id+'.md';
let paths=[...new Set([...providers.map(p=>resultIndex[p+'|'+id]),artifactIndex[referencePath]])];
@@ -3002,8 +3024,8 @@ def target_body_count(provider):
if(typeof output!=='string')throw Error('方法输出缺失:'+provider);
let tr=document.createElement('tr'),td=document.createElement('td'),link=document.createElement('a');
link.href='results/index.html?provider='+provider+'&id='+id;link.textContent=names[provider]||provider;
- td.append(link);tr.append(td);td=document.createElement('td');td.textContent=row.diagnosis;tr.append(td);
- for(let metric of metrics){td=document.createElement('td');let entry=row.metrics?.[metric];td.textContent=(!entry?.success||entry.score==null)?'—':(entry.score*100).toFixed(1)+'%';tr.append(td)}
+ td.dataset.sort=names[provider]||provider;td.append(link);tr.append(td);td=document.createElement('td');td.dataset.sort=row.diagnosis;td.textContent=row.diagnosis;tr.append(td);
+ for(let metric of metrics){td=document.createElement('td');let entry=row.metrics?.[metric];td.dataset.sort=(!entry?.success||entry.score==null)?'':String(entry.score);td.textContent=(!entry?.success||entry.score==null)?'—':(entry.score*100).toFixed(1)+'%';tr.append(td)}
body.append(tr);
let panel=document.createElement('article');panel.className='panel result-output';
let title=document.createElement('h2');title.textContent=names[provider]||provider;
@@ -3016,20 +3038,37 @@ def target_body_count(provider):
}
document.addEventListener('DOMContentLoaded',()=>{if(document.getElementById('result-title'))renderResult();if(document.getElementById('case-id'))renderCase()});
"""
+ def js_string(value):
+ return (json.dumps(value, ensure_ascii=False)
+ .replace('<', '\\u003c').replace('>', '\\u003e').replace('&', '\\u0026')
+ .replace('\u2028', '\\u2028').replace('\u2029', '\\u2029'))
+ js = js.replace("__PROVIDER_LABELS_JSON__", js_string(labels))
js += """
-async function openDiagnosis(button){let tr=button.closest('tr[data-result]'),dialog=document.getElementById('diagnosis-dialog'),provider=tr?.dataset.provider,id=tr?.dataset.caseId;if(!dialog||!provider||!id)return;let put=(name,value)=>{document.getElementById('diagnosis-'+name).textContent=value};put('title','正在读取诊断…');put('error','');put('log','');put('cause','');put('treatment','');dialog.showModal();try{let index=await loadJson('result-shard-index.json'),key=provider+'|'+id,shard=await loadJson(index[key]),row=shard.results[key];if(!row)throw Error('结果证据缺失');put('title',id+' · '+row.diagnosis);put('stage',row.error_type?'失败阶段:'+row.error_type:'诊断类别:'+row.diagnosis);put('error','程序错误:'+(row.error_message||'无'));put('log',row.provider_metadata?.stderr||row.provider_metadata?.browser_stderr||row.error_message||'无日志');put('cause','复核原因:'+(row.review?.cause||'未独立复核'));put('treatment','处理:'+(row.review?.treatment||'未记录')+';状态:'+(row.review?.status||'未记录'));document.getElementById('diagnosis-result').href='results/index.html?provider='+provider+'&id='+id;document.getElementById('diagnosis-output').href='viewer/index.html?path='+encodeURIComponent(row.artifact_path);document.getElementById('diagnosis-case').href='cases/index.html?id='+id}catch(error){put('title','诊断读取失败');put('error',error.message)}}
-document.addEventListener('DOMContentLoaded',()=>{let dialog=document.getElementById('diagnosis-dialog');if(!dialog)return;document.getElementById('diagnosis-close').addEventListener('click',()=>dialog.close());document.addEventListener('click',event=>{let button=event.target.closest('button[data-diagnose]');if(button)openDiagnosis(button)})});
+let diagnosisFocus=null;async function openDiagnosis(button){let tr=button.closest('tr[data-result]'),dialog=document.getElementById('diagnosis-dialog'),provider=tr?.dataset.provider,id=tr?.dataset.caseId;if(!dialog||!provider||!id)return;diagnosisFocus=button;let put=(name,value)=>{document.getElementById('diagnosis-'+name).textContent=value};put('title','正在读取诊断…');put('error','');put('log','');put('cause','');put('treatment','');if(typeof dialog.showModal==='function')dialog.showModal();else{dialog.setAttribute('open','');dialog.setAttribute('aria-modal','true')}try{let index=await loadJson('result-shard-index.json'),key=provider+'|'+id,shard=await loadJson(index[key]),row=shard.results[key];if(!row)throw Error('结果证据缺失');put('title',id+' · '+row.diagnosis);put('stage',row.error_type?'失败阶段:'+row.error_type:'诊断类别:'+row.diagnosis);put('error','程序错误:'+(row.error_message||'无'));put('log',row.provider_metadata?.stderr||row.provider_metadata?.browser_stderr||row.error_message||'无日志');put('cause','复核原因:'+(row.review?.cause||'未独立复核'));put('treatment','处理:'+(row.review?.treatment||'未记录').replace(/[。;]+$/,'')+';状态:'+(row.review?.status||'未记录'));document.getElementById('diagnosis-result').href='results/index.html?provider='+provider+'&id='+id;document.getElementById('diagnosis-output').href='viewer/index.html?path='+encodeURIComponent(row.artifact_path);document.getElementById('diagnosis-case').href='cases/index.html?id='+id}catch(error){put('title','诊断读取失败');put('error',error.message)}}
+document.addEventListener('DOMContentLoaded',()=>{let dialog=document.getElementById('diagnosis-dialog');if(!dialog)return;document.getElementById('diagnosis-close').addEventListener('click',()=>dialog.close());dialog.addEventListener('close',()=>diagnosisFocus?.focus())});
"""
- css += "code,#result-error{overflow-wrap:anywhere;word-break:break-word}dialog{width:min(760px,calc(100% - 24px));max-height:85vh;overflow:auto;border:1px solid #9aacc0;border-radius:12px;background:white;color:#172637;padding:22px}dialog::backdrop{background:#0b172bc9}body.dark dialog{background:#1c2b3b;color:#edf3fa;border-color:#61768b}dialog .actions{margin-top:16px}dialog pre{max-height:28vh}td button[data-diagnose]{font-size:14px;padding:2px 6px}"
+ css += "code,#result-error{overflow-wrap:anywhere;word-break:break-word}.sort-button{appearance:none;border:0;background:transparent;color:inherit;padding:0;font-weight:700;cursor:pointer;text-align:left}.sort-button:hover,.sort-button:focus-visible{text-decoration:underline}th[aria-sort=ascending] .sort-button::after{content:' ↑'}th[aria-sort=descending] .sort-button::after{content:' ↓'}dialog{width:min(760px,calc(100% - 24px));max-height:85vh;overflow:auto;border:1px solid #9aacc0;border-radius:12px;background:white;color:#172637;padding:22px}dialog::backdrop{background:#0b172bc9}body.dark dialog{background:#1c2b3b;color:#edf3fa;border-color:#61768b}dialog .actions{margin-top:16px}dialog pre{max-height:28vh}td button[data-diagnose]{font-size:14px;padding:2px 6px}.presentation-slide{background:white;border:1px solid #d8e1eb;border-radius:16px;padding:24px;margin:28px 0}.dark .presentation-slide{background:#1c2b3b;border-color:#38506a}.presentation-slide figure{margin:12px 0}.slide-facts{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:10px;margin-top:14px}.slide-facts div{padding:12px;border-radius:10px;background:#eef4fa}.dark .slide-facts div{background:#263d54}.slide-facts strong{display:block;color:#116b73;margin-bottom:4px}.dark .slide-facts strong{color:#75ddd1}@media(max-width:800px){.slide-facts{grid-template-columns:1fr 1fr}}@media(max-width:520px){.slide-facts{grid-template-columns:1fr}}@media print{header,footer{display:none}.presentation-slide{break-after:page;min-height:95vh;border:0;margin:0;padding:12mm}figure img{max-height:70vh;object-fit:contain}}"
(out / "assets").mkdir(exist_ok=True)
(out / "assets/style.css").write_text(css, encoding="utf-8")
(out / "assets/report.js").write_text(js, encoding="utf-8")
+ asset_version = hashlib.sha256((css + "\0" + js).encode()).hexdigest()[:12]
def anchor(path, label):
return '' + esc(label) + ''
- def table(headers, rows):
- return '
' + ''.join("
" + esc(x) + "
" for x in headers) + '
' + ''.join(rows) + '
'
+ def table(headers, rows, sort_types=None, table_id=None):
+ sort_types = sort_types or [None] * len(headers)
+ if len(sort_types) != len(headers):
+ raise ValueError("Table sort contract differs from its columns")
+ header_cells = []
+ for label, sort_type in zip(headers, sort_types):
+ if sort_type:
+ header_cells.append('