Skip to content

Answer "which model is best" on the homepage and check outbound links in CI - #218

Merged
vassiliphilippov merged 1 commit into
mainfrom
agent/homepage-answer-block
Jul 26, 2026
Merged

Answer "which model is best" on the homepage and check outbound links in CI#218
vassiliphilippov merged 1 commit into
mainfrom
agent/homepage-answer-block

Conversation

@vassiliphilippov

@vassiliphilippov vassiliphilippov commented Jul 26, 2026

Copy link
Copy Markdown
Contributor

What

The homepage now opens with the question and answers it in one self-contained paragraph:

Which LLM is best at word sense disambiguation?

As of 25 July 2026, the best verified result on lexEN v1 is 95.60% (95% CI 95.00–96.17), from GPT-5.5 at xhigh reasoning effort under registered prompt p001 — 4,647 of 4,861 polysemous English items. Only Claude Fable 5 is statistically indistinguishable from it (95.21%, McNemar p = 0.20); Gemini 3.1 Pro, GPT-5.6 and Claude Opus 5 all fall significantly below. WordNet's most-frequent-sense heuristic scores 61.55% on the same items; among supervised systems ConSeC, trained on the original human labels, reaches 84.88%, while Glite's own LENS, retrained on model-relabelled SemCor, reaches 89.69%. Figures use the default labels — lexEN v1 gold at WordNet fine granularity; coarser sense inventories score substantially higher. Every number is recomputed in CI from the stored raw API responses.

Five links carry a reader from each claim to the artifact behind it: the top run page, the registered prompt, the lexEN repo, /label-schemes/, /coarsening/.

Why it is hand-written, and how that is kept safe

The interesting sentences are judgements a template cannot make. Which models tie is an exact McNemar test over stored per-item predictions, not accuracy proximity — runs 0.4 points apart have tested both ways. Worth noting: the paper's "the top three families are statistically indistinguishable" no longer holds on current data. Only Claude Fable 5 ties (p = 0.20); Gemini 3.1 Pro (p = 0.031), GPT-5.6 (p = 0.019) and Claude Opus 5 (p = 0.004) all differ.

Hand-written prose drifts, so:

  • tools/check_homepage_answer.py verifies every figure against built site data and fails on drift. Demonstrated: injecting a bogus accuracy and model name produces
    99.99%: matches no current run, baseline or confidence bound
    top accuracy: expected '95.60%', not present in the block
    top model: expected 'GPT-5.5', not present in the block
    
  • .agents/skills/write-homepage-answer records how to rewrite it, including the traps I hit while drafting — recompute rather than copy, always name the scheme, never call a Glite artifact "strongest" without the training-data qualifier.

Scheme qualifier

Accuracy here is meaningless without its label scheme: the same run scores 95.60% under lexen_fine and 85.39% under raganato_fine, and the top model changes with it. The prose names its scheme, so it stays true in every selector state.

Also

  • Title → SenseBench (WSD): LLM Word Sense Disambiguation Leaderboard, committing to the (WSD) house form against arXiv:2605.10576. Meta description generated from live counts.
  • lychee link-checker — nothing in CI inspected outbound links, which is why the glite-lens 404 survived. Covers README, docs/, and baselines.py (the dead link was a Python string, not markdown). All 22 real URLs verified 200; localhost and http://... examples excluded.

… in CI

Nothing on the site answered the question people actually ask. The top
accuracy existed only as an unattributed stat tile that a reader -- or a
retrieval system -- had to join to a table row across 200 KB of HTML,
and the homepage title was the generic "SenseBench Leaderboard".

The homepage now opens the leaderboard with an H2 phrased as the
question and one self-contained paragraph answering it: the top verified
result with its confidence interval, the run and prompt that produced it,
which models are statistically tied with it, and where the MFS, ConSeC
and LENS baselines sit. Five links carry a reader from each claim to the
artifact behind it.

The paragraph is written by hand rather than generated. Its interesting
sentences are judgements a format string cannot make: which models tie
is an exact McNemar test over stored per-item predictions, not accuracy
proximity -- runs 0.4 points apart have tested both ways -- and the gap
between ConSeC and LENS is a training-data difference worth explaining
rather than a ranking worth asserting. Only Claude Fable 5 currently
ties with GPT-5.5 at p = 0.20; the paper's "top three families are
statistically indistinguishable" no longer holds on current data.

Hand-written prose goes stale, so tools/check_homepage_answer.py
verifies every figure in the block against the built site data and fails
when one drifts, and .agents/skills/write-homepage-answer records how to
rewrite it -- including the traps: recompute rather than copy, always
name the label scheme, and never call a Glite artifact "strongest"
without the qualifier.

Accuracy here is meaningless without its scheme. The same run scores
95.60% under lexEN fine and 85.39% under Raganato fine, and the top
model changes with it, so the prose names its scheme and stays true in
every selector state.

The title now carries the head term and the "(WSD)" disambiguator
against the remote-sensing benchmark of the same name, and the meta
description is generated from live counts so it cannot drift.

Separately, nothing in CI inspected outbound links; the glite-lens 404
sat on the leaderboard undetected because it was a Python string rather
than a markdown link. A lychee job now covers README, docs and
baselines.py, with localhost and placeholder examples excluded.

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Jul 26, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

The homepage now renders dynamic leaderboard metadata, a title update, and a verified answer block. A new validator compares answer-block claims with generated leaderboard data and the latest run date. Homepage tests cover rendered content and the default scheme. Pages CI runs the validator, CI adds link checking, and a skill document defines the regeneration workflow.

Estimated code review effort: 3 (Moderate) | ~20 minutes

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description check ✅ Passed The description directly matches the homepage answer, verification script, skills doc, and link-checking changes.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Destructive Migrations ✅ Passed Not a Django migration PR: no migration files changed and the repo shows no Django/migrate indicators.
Mobile Localization String Quality Review ✅ Passed No mobile/ files were changed, so the localization review is not applicable to this PR.

Comment @coderabbitai help to get the list of available commands.

@vassiliphilippov
vassiliphilippov merged commit 259b8f9 into main Jul 26, 2026
4 of 5 checks passed
@vassiliphilippov
vassiliphilippov deleted the agent/homepage-answer-block branch July 26, 2026 12:26

@coderabbitai coderabbitai 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.

Actionable comments posted: 3

🧹 Nitpick comments (1)
tests/test_site.py (1)

771-774: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Assert the scheme qualifier in rendered HTML.

This only compares a constant; the answer block can omit or misname lexEN v1 / WordNet fine-grained scoring and the test still passes. Assert the qualifier in the built index_html, or make the homepage checker enforce it.


ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: e864d618-844b-44db-8e6c-f08f55bbc710

📥 Commits

Reviewing files that changed from the base of the PR and between 27eccc4 and 850d754.

📒 Files selected for processing (7)
  • .agents/skills/write-homepage-answer/SKILL.md
  • .github/workflows/ci.yml
  • .github/workflows/pages.yml
  • src/sensebench/site/build.py
  • src/sensebench/site/templates/index.html.j2
  • tests/test_site.py
  • tools/check_homepage_answer.py
📜 Review details
⏰ Context from checks skipped due to timeout. (1)
  • GitHub Check: build
🧰 Additional context used
🪛 ast-grep (0.44.1)
tools/check_homepage_answer.py

[warning] 63-63: XPath query is request-/variable-derived; use parameterized XPath to prevent injection.
Context: PERCENT_PATTERN.findall(block)
Note: [CWE-643] Improper Neutralization of Data within XPath Expressions ('XPath Injection').

(xpath-injection-python)


[warning] 90-90: XPath query is request-/variable-derived; use parameterized XPath to prevent injection.
Context: TIME_PATTERN.findall(block)
Note: [CWE-643] Improper Neutralization of Data within XPath Expressions ('XPath Injection').

(xpath-injection-python)

🪛 LanguageTool
.agents/skills/write-homepage-answer/SKILL.md

[style] ~93-~93: Three successive sentences begin with the same word. Consider rewording the sentence or use a thesaurus to find a synonym.
Context: ...and never link the same target twice. * NEVER rewrite the rest of the page "for AI re...

(ENGLISH_WORD_REPEAT_BEGINNING_RULE)

🪛 zizmor (1.26.1)
.github/workflows/ci.yml

[warning] 25-27: credential persistence through GitHub Actions artifacts (artipacked): does not set persist-credentials: false

(artipacked)


[warning] 22-38: overly broad permissions (excessive-permissions): default permissions used due to no permissions: block

(excessive-permissions)

🔇 Additional comments (6)
src/sensebench/site/build.py (1)

1659-1660: LGTM!

src/sensebench/site/templates/index.html.j2 (1)

42-60: LGTM!

tests/test_site.py (2)

23-23: LGTM!

Also applies to: 171-173, 175-175


745-768: LGTM!

tools/check_homepage_answer.py (1)

102-125: LGTM!

.github/workflows/pages.yml (1)

27-27: LGTM!

Comment thread .github/workflows/ci.yml
Comment on lines +22 to +38
links:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
# Scoped to the files that carry outbound links we control. The Glite LENS 404 lived
# in a Python string, not a markdown link, so baselines.py is included deliberately.
- uses: lycheeverse/lychee-action@v2
with:
# --exclude-all-private drops the localhost vLLM example; the second exclude drops
# the literal `http://...` placeholder in the self-hosted docs.
args: >-
--no-progress --max-retries 3 --exclude-all-private
--exclude '^http://\.\.\.'
README.md docs src/sensebench/leaderboard/baselines.py
fail: true
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔒 Security & Privacy | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- ci.yml ---'
nl -ba .github/workflows/ci.yml | sed -n '1,120p'

printf '\n%s\n' '--- permissions references ---'
rg -n "permissions:|persist-credentials|GITHUB_TOKEN|lychee-action" .github/workflows -n

Repository: GliteTech/sensebench

Length of output: 213


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- ci.yml ---'
nl -ba .github/workflows/ci.yml | sed -n '1,160p'

printf '\n%s\n' '--- permissions references ---'
rg -n "permissions:|persist-credentials|GITHUB_TOKEN|lychee-action" .github/workflows

Repository: GliteTech/sensebench

Length of output: 213


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- .github/workflows/ci.yml ---'
sed -n '1,220p' .github/workflows/ci.yml

printf '\n%s\n' '--- workflow-wide token/checkout settings ---'
rg -n "permissions:|persist-credentials|GITHUB_TOKEN|actions/checkout@v4|lychee-action@v2" .github/workflows .github

Repository: GliteTech/sensebench

Length of output: 3183


Tighten token scope for the link check. lychee-action doesn’t need a writable token here, and actions/checkout keeps credentials in the repo by default. Add permissions: contents: read and persist-credentials: false.

🧰 Tools
🪛 zizmor (1.26.1)

[warning] 25-27: credential persistence through GitHub Actions artifacts (artipacked): does not set persist-credentials: false

(artipacked)


[warning] 22-38: overly broad permissions (excessive-permissions): default permissions used due to no permissions: block

(excessive-permissions)

Source: Linters/SAST tools

Comment on lines +437 to +446
def _index_description(*, summary: SiteSummary) -> str:
dataset_labels = ", ".join(
_dataset_version_label(version) for version in summary.dataset_versions
)
dataset_clause = "" if len(dataset_labels) == 0 else f" on {dataset_labels}"
return (
"Verified leaderboard for English word sense disambiguation: "
f"{summary.verified_run_count} audited runs across {summary.model_count} models"
f"{dataset_clause}. Top accuracy {_format_percent(summary.top_accuracy)}."
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Pluralise the run label for singleton sites.

This always renders 1 audited runs; the new fixture currently locks that typo. Use singular/plural selection and update tests/test_site.py:174.

Proposed fix
 def _index_description(*, summary: SiteSummary) -> str:
+    run_word = "run" if summary.verified_run_count == 1 else "runs"
     dataset_labels = ", ".join(
         _dataset_version_label(version) for version in summary.dataset_versions
     )
     dataset_clause = "" if len(dataset_labels) == 0 else f" on {dataset_labels}"
     return (
         "Verified leaderboard for English word sense disambiguation: "
-        f"{summary.verified_run_count} audited runs across {summary.model_count} models"
+        f"{summary.verified_run_count} audited {run_word} across {summary.model_count} models"
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
def _index_description(*, summary: SiteSummary) -> str:
dataset_labels = ", ".join(
_dataset_version_label(version) for version in summary.dataset_versions
)
dataset_clause = "" if len(dataset_labels) == 0 else f" on {dataset_labels}"
return (
"Verified leaderboard for English word sense disambiguation: "
f"{summary.verified_run_count} audited runs across {summary.model_count} models"
f"{dataset_clause}. Top accuracy {_format_percent(summary.top_accuracy)}."
)
def _index_description(*, summary: SiteSummary) -> str:
run_word = "run" if summary.verified_run_count == 1 else "runs"
dataset_labels = ", ".join(
_dataset_version_label(version) for version in summary.dataset_versions
)
dataset_clause = "" if len(dataset_labels) == 0 else f" on {dataset_labels}"
return (
"Verified leaderboard for English word sense disambiguation: "
f"{summary.verified_run_count} audited {run_word} across {summary.model_count} models"
f"{dataset_clause}. Top accuracy {_format_percent(summary.top_accuracy)}."
)

Comment on lines +24 to +98
BLOCK_PATTERN: re.Pattern[str] = re.compile(
r'<section class="answer-block".*?</section>', re.DOTALL
)
PERCENT_PATTERN: re.Pattern[str] = re.compile(r"(\d+\.\d{2})%")
COUNT_PATTERN: re.Pattern[str] = re.compile(r"(\d{1,3}(?:,\d{3})+|\b\d{2,4}\b)")
TIME_PATTERN: re.Pattern[str] = re.compile(r'<time datetime="(\d{4}-\d{2}-\d{2})"')

REQUIRED_BASELINE_LABELS: tuple[str, ...] = ("MFS (WordNet first sense)", "ConSeC", "Glite LENS")


@dataclass(frozen=True, slots=True)
class Failure:
claim: str
detail: str


def _pct(value: float) -> str:
return f"{value * 100:.2f}"


def _check(*, index_html: Path, leaderboard_json: Path) -> list[Failure]:
html_text = index_html.read_text(encoding="utf-8")
match = BLOCK_PATTERN.search(html_text)
if match is None:
return [Failure("answer block", f'no <section class="answer-block"> in {index_html}')]
block = match.group(0)

payload = json.loads(leaderboard_json.read_text(encoding="utf-8"))
entries = payload["entries"]
baselines = {row["label"]: row for row in payload.get("baselines", [])}
summary = payload["summary"]
top = entries[0]

failures: list[Failure] = []

# Every figure the prose is allowed to state, derived from live data.
allowed = {_pct(entry["accuracy"]) for entry in entries if entry.get("accuracy") is not None}
allowed |= {_pct(row["accuracy"]) for row in baselines.values() if row.get("accuracy")}
allowed |= {_pct(top["accuracy_ci"]["low"]), _pct(top["accuracy_ci"]["high"])}

for stated in PERCENT_PATTERN.findall(block):
if stated not in allowed:
failures.append(
Failure(f"{stated}%", "matches no current run, baseline or confidence bound")
)

def require(label: str, value: str) -> None:
if value not in block:
failures.append(Failure(label, f"expected {value!r}, not present in the block"))

require("top accuracy", f"{_pct(top['accuracy'])}%")
require("CI low", f"{_pct(top['accuracy_ci']['low'])}")
require("CI high", f"{_pct(top['accuracy_ci']['high'])}")
require("top model", top["display_label"] or top["model"])
require("correct count", f"{top['correct_count']:,}")
require("item count", f"{top['item_count']:,}")
require("run count", str(summary["verified_run_count"]))
require("model count", str(summary["model_count"]))

for label in REQUIRED_BASELINE_LABELS:
row = baselines.get(label)
if row is None:
failures.append(Failure(f"baseline {label}", "absent from the built site data"))
continue
require(f"baseline {label}", f"{_pct(row['accuracy'])}%")

latest_run_date = max(entry["created_at"] for entry in entries)[:10]
stated_dates = TIME_PATTERN.findall(block)
if len(stated_dates) == 0:
failures.append(Failure("<time>", "the block states no machine-readable date"))
elif stated_dates[0] != latest_run_date:
failures.append(
Failure("<time>", f"states {stated_dates[0]}, latest run is {latest_run_date}")
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

Complete the homepage-integrity contract.

The checker does not validate several handwritten claims, so the skill’s “every figure is machine-checked” guarantee is currently false.

  • tools/check_homepage_answer.py#L24-L98: validate exact label/value associations, statistical comparisons, scheme and top-run metadata, links, and both date representations.
  • .agents/skills/write-homepage-answer/SKILL.md#L14-L16: retain the guarantee only after those checks exist.
  • tests/test_site.py#L771-L774: assert the scheme qualifier in rendered HTML rather than checking only the constant.
🧰 Tools
🪛 ast-grep (0.44.1)

[warning] 63-63: XPath query is request-/variable-derived; use parameterized XPath to prevent injection.
Context: PERCENT_PATTERN.findall(block)
Note: [CWE-643] Improper Neutralization of Data within XPath Expressions ('XPath Injection').

(xpath-injection-python)


[warning] 90-90: XPath query is request-/variable-derived; use parameterized XPath to prevent injection.
Context: TIME_PATTERN.findall(block)
Note: [CWE-643] Improper Neutralization of Data within XPath Expressions ('XPath Injection').

(xpath-injection-python)

📍 Affects 3 files
  • tools/check_homepage_answer.py#L24-L98 (this comment)
  • .agents/skills/write-homepage-answer/SKILL.md#L14-L16
  • tests/test_site.py#L771-L774

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