From 7f364209784795a173fa1dac81ea1c78b2427118 Mon Sep 17 00:00:00 2001 From: xxllovemkm Date: Thu, 14 May 2026 00:30:18 +0800 Subject: [PATCH 1/6] feat: add webpage quality checks --- SKILL.md | 1 + schemas/output.schema.json | 21 +++++++++++++++++++++ skill.yaml | 2 +- tests/golden.jsonl | 1 + 4 files changed, 24 insertions(+), 1 deletion(-) diff --git a/SKILL.md b/SKILL.md index 684258b..97caab0 100644 --- a/SKILL.md +++ b/SKILL.md @@ -52,6 +52,7 @@ In the final response, include: - The generated/updated `index.html` path. - The major modules included. - The important figures/tables included. +- The quality checks performed for links, HTML sanity, responsive layout, asset independence, and visual consistency. - Validation performed and remaining risks. ## Reference Files diff --git a/schemas/output.schema.json b/schemas/output.schema.json index 298d8f4..f162c3a 100644 --- a/schemas/output.schema.json +++ b/schemas/output.schema.json @@ -70,6 +70,27 @@ } } }, + "quality_checks": { + "type": "array", + "items": { + "type": "object", + "additionalProperties": false, + "required": ["name", "status"], + "properties": { + "name": { + "type": "string", + "enum": ["links", "html_sanity", "responsive_layout", "asset_independence", "visual_consistency"] + }, + "status": { + "type": "string", + "enum": ["pass", "fail", "not_run"] + }, + "notes": { + "type": "string" + } + } + } + }, "risks": { "type": "array", "items": { diff --git a/skill.yaml b/skill.yaml index f40b6b1..dc8cc6a 100644 --- a/skill.yaml +++ b/skill.yaml @@ -1,5 +1,5 @@ name: paper-webpage-builder -version: 0.1.0 +version: 0.2.0 description: Build polished single-page academic project webpages from paper sources, figures, references, and validation scripts. prompts: skill: SKILL.md diff --git a/tests/golden.jsonl b/tests/golden.jsonl index 07ae355..48785ae 100644 --- a/tests/golden.jsonl +++ b/tests/golden.jsonl @@ -1,2 +1,3 @@ {"case_id":"latex-project-page","input":{"paper_source":{"path":"paper.tex","kind":"latex_project","has_figures":true,"has_tables":true},"target_output":{"directory":"web","filename":"index.html"},"goals":["project landing page","show method and results"],"constraints":["single-page html","local assets only"]},"expected":{"index_html_path":"web/index.html","modules":[{"name":"Hero","purpose":"Present title, authors, venue, resource links, and primary paper claim.","content_sources":["paper title","abstract","project links"]},{"name":"Method","purpose":"Explain the core technical workflow using paper figures and concise callouts.","content_sources":["method section","figures"]},{"name":"Results","purpose":"Summarize main quantitative results and important tables.","content_sources":["results section","tables"]}],"assets":[{"path":"web/figures/overview.png","role":"figure"},{"path":"web/paper.pdf","role":"paper"}],"validation":{"link_check":"pass","html_sanity":"pass","responsive_check":"not_run"},"risks":["Browser screenshot validation still needs to be run."]},"match_mode":"schema_only"} {"case_id":"pdf-with-assets-page","input":{"paper_source":{"path":"paper.pdf","kind":"pdf_with_assets","has_figures":true,"has_tables":false},"target_output":{"directory":"site","filename":"index.html"},"goals":["polished academic page","highlight dataset contribution"],"constraints":["reuse provided figures","avoid generic stock visuals"]},"expected":{"index_html_path":"site/index.html","modules":[{"name":"Hero","purpose":"Introduce the paper and primary contribution.","content_sources":["PDF metadata","abstract"]},{"name":"Dataset","purpose":"Explain dataset scope, examples, and usage value.","content_sources":["paper figures","dataset description"]}],"assets":[{"path":"site/figures/dataset-examples.png","role":"figure"}],"validation":{"link_check":"pass","html_sanity":"not_run","responsive_check":"not_run"},"risks":["PDF-only extraction may miss author affiliations or table details."]},"match_mode":"schema_only"} +{"case_id":"existing-webpage-refresh","input":{"paper_source":{"path":"web/index.html","kind":"existing_webpage","has_figures":true,"has_tables":true},"target_output":{"directory":"web","filename":"index.html"},"goals":["refresh visual hierarchy","preserve existing resource links"],"constraints":["do not break local assets","keep page self-contained"]},"expected":{"index_html_path":"web/index.html","modules":[{"name":"Hero","purpose":"Refresh top-level narrative while preserving primary paper links.","content_sources":["existing webpage","paper abstract"]},{"name":"Quality Checks","purpose":"Summarize validation and remaining review risks.","content_sources":["link checker","browser inspection"]}],"assets":[{"path":"web/index.html","role":"other"}],"validation":{"link_check":"pass","html_sanity":"pass","responsive_check":"pass"},"quality_checks":[{"name":"links","status":"pass","notes":"Local src, href, and data-figure references were checked."},{"name":"asset_independence","status":"pass","notes":"The page does not depend on the source paper directory."},{"name":"visual_consistency","status":"not_run","notes":"Manual design review is still required."}],"risks":["Visual consistency should be reviewed after screenshots are captured."]},"match_mode":"schema_only"} From 70d5845b332ca12f7f600bc1c837a405905ccb2f Mon Sep 17 00:00:00 2001 From: xxllovemkm Date: Fri, 15 May 2026 21:04:47 +0800 Subject: [PATCH 2/6] test: add runner-backed golden tests --- .github/pull_request_template.md | 2 + .github/workflows/sit-ci.yaml | 12 +- reports/real-runner-loop/sit-report.html | 334 ++++++++++++++++++ reports/real-runner-loop/sit-report.json | 49 +++ reports/real-runner-loop/sit-report.md | 43 +++ reports/real-runner-loop/sit-summary.md | 36 ++ .../real-runner-loop/sit-test-run-failure.txt | 7 + reports/real-runner-loop/sit-test-run.json | 49 +++ reports/real-runner-loop/sit-test-run.txt | 7 + scripts/sit_run_case.py | 135 +++++++ skill.yaml | 2 + tests/golden.jsonl | 6 +- 12 files changed, 677 insertions(+), 5 deletions(-) create mode 100644 reports/real-runner-loop/sit-report.html create mode 100644 reports/real-runner-loop/sit-report.json create mode 100644 reports/real-runner-loop/sit-report.md create mode 100644 reports/real-runner-loop/sit-summary.md create mode 100644 reports/real-runner-loop/sit-test-run-failure.txt create mode 100644 reports/real-runner-loop/sit-test-run.json create mode 100644 reports/real-runner-loop/sit-test-run.txt create mode 100644 scripts/sit_run_case.py diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index 5cb887c..e04be34 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -5,6 +5,7 @@ Run locally before opening the PR: ```bash sit validate sit test +sit test --run sit pr-summary origin/main..HEAD ``` @@ -12,5 +13,6 @@ sit pr-summary origin/main..HEAD - [ ] `sit validate` passes - [ ] `sit test` passes +- [ ] `sit test --run` passes - [ ] Generated webpage assets were link-checked when relevant - [ ] Breaking schema or output-contract changes are explained diff --git a/.github/workflows/sit-ci.yaml b/.github/workflows/sit-ci.yaml index 1ef8c96..d43d4cb 100644 --- a/.github/workflows/sit-ci.yaml +++ b/.github/workflows/sit-ci.yaml @@ -16,8 +16,16 @@ jobs: fetch-depth: 0 - name: Install sit run: python -m pip install git+https://github.com/OpenRaiser/SitHub.git - - run: sit validate "$SIT_PACKAGE_DIR" - - run: sit test "$SIT_PACKAGE_DIR" + - name: Validate Skill package + run: sit validate "$SIT_PACKAGE_DIR" + - name: Run static golden tests + run: sit test "$SIT_PACKAGE_DIR" + - name: Run behavior regression tests + shell: bash + run: | + set -o pipefail + mkdir -p "$SIT_ARTIFACT_DIR" + sit test "$SIT_PACKAGE_DIR" --run | tee "$SIT_ARTIFACT_DIR/sit-test-run.txt" - name: Write SitHub summary if: always() run: | diff --git a/reports/real-runner-loop/sit-report.html b/reports/real-runner-loop/sit-report.html new file mode 100644 index 0000000..a4661a2 --- /dev/null +++ b/reports/real-runner-loop/sit-report.html @@ -0,0 +1,334 @@ + + + + + +paper-webpage-builder 0.2.0 SitHub Report + + + +
+
+
+

SitHub semantic control report

+

paper-webpage-builder

+

Version 0.2.0 - 2026-05-15

+
+
+Diff risk +no-compare +Suggested bump: none +
+
+
+
+Validation +pass +
+
+Golden Tests +pass +
+
+Cases Passed +3/3 +
+
+Version Bump +none +
+
+
+
+

Golden Test Progress

+3/3 +
+
+
+
+
+
+
+

Validation

+
    +
  • OK name: paper-webpage-builder
  • +
  • OK version: 0.2.0
  • +
  • OK manifest exists: /mnt/shared-storage-user/xuxinglong-p/paper-webpage-builder/skill.yaml
  • +
  • OK prompt.skill exists: /mnt/shared-storage-user/xuxinglong-p/paper-webpage-builder/SKILL.md
  • +
  • OK prompt.module_patterns exists: /mnt/shared-storage-user/xuxinglong-p/paper-webpage-builder/references/module_patterns.md
  • +
  • OK prompt.design_principles exists: /mnt/shared-storage-user/xuxinglong-p/paper-webpage-builder/references/design_principles.md
  • +
  • OK schema.input exists: /mnt/shared-storage-user/xuxinglong-p/paper-webpage-builder/schemas/input.schema.json
  • +
  • OK schema.output exists: /mnt/shared-storage-user/xuxinglong-p/paper-webpage-builder/schemas/output.schema.json
  • +
  • OK test.golden exists: /mnt/shared-storage-user/xuxinglong-p/paper-webpage-builder/tests/golden.jsonl
  • +
  • OK schema.input JSON schema valid
  • +
  • OK schema.output JSON schema valid
  • +
  • OK test.golden JSONL parsed: 3 cases
  • +
  • OK command.run_case: configured
  • +
+
+
+

Golden Tests

+
    +
  • OK latex-project-page: partial match passed
  • +
  • OK pdf-with-assets-page: partial match passed
  • +
  • OK existing-webpage-refresh: partial match passed
  • +
  • SUMMARY 3/3 golden cases passed
  • +
+
+
+
+
+

Reproduce

+local commands +
+

+python3 -m sit.cli validate /mnt/shared-storage-user/xuxinglong-p/paper-webpage-builder
+python3 -m sit.cli test /mnt/shared-storage-user/xuxinglong-p/paper-webpage-builder
+
+
+ +
+ + diff --git a/reports/real-runner-loop/sit-report.json b/reports/real-runner-loop/sit-report.json new file mode 100644 index 0000000..e21eb46 --- /dev/null +++ b/reports/real-runner-loop/sit-report.json @@ -0,0 +1,49 @@ +{ + "schema_version": "sit.report.v1", + "date": "2026-05-15", + "package": { + "name": "paper-webpage-builder", + "version": "0.2.0", + "description": "Build polished single-page academic project webpages from paper sources, figures, references, and validation scripts.", + "root": "/mnt/shared-storage-user/xuxinglong-p/paper-webpage-builder", + "manifest": "/mnt/shared-storage-user/xuxinglong-p/paper-webpage-builder/skill.yaml" + }, + "validation": { + "status": "pass", + "ok": true, + "messages": [ + "OK name: paper-webpage-builder", + "OK version: 0.2.0", + "OK manifest exists: /mnt/shared-storage-user/xuxinglong-p/paper-webpage-builder/skill.yaml", + "OK prompt.skill exists: /mnt/shared-storage-user/xuxinglong-p/paper-webpage-builder/SKILL.md", + "OK prompt.module_patterns exists: /mnt/shared-storage-user/xuxinglong-p/paper-webpage-builder/references/module_patterns.md", + "OK prompt.design_principles exists: /mnt/shared-storage-user/xuxinglong-p/paper-webpage-builder/references/design_principles.md", + "OK schema.input exists: /mnt/shared-storage-user/xuxinglong-p/paper-webpage-builder/schemas/input.schema.json", + "OK schema.output exists: /mnt/shared-storage-user/xuxinglong-p/paper-webpage-builder/schemas/output.schema.json", + "OK test.golden exists: /mnt/shared-storage-user/xuxinglong-p/paper-webpage-builder/tests/golden.jsonl", + "OK schema.input JSON schema valid", + "OK schema.output JSON schema valid", + "OK test.golden JSONL parsed: 3 cases", + "OK command.run_case: configured" + ] + }, + "golden_tests": { + "status": "pass", + "ok": true, + "passed": 3, + "total": 3, + "summary": "SUMMARY 3/3 golden cases passed", + "messages": [ + "OK latex-project-page: partial match passed", + "OK pdf-with-assets-page: partial match passed", + "OK existing-webpage-refresh: partial match passed", + "SUMMARY 3/3 golden cases passed" + ] + }, + "diff": null, + "reproducibility": { + "validate": "python3 -m sit.cli validate /mnt/shared-storage-user/xuxinglong-p/paper-webpage-builder", + "test": "python3 -m sit.cli test /mnt/shared-storage-user/xuxinglong-p/paper-webpage-builder", + "diff": null + } +} diff --git a/reports/real-runner-loop/sit-report.md b/reports/real-runner-loop/sit-report.md new file mode 100644 index 0000000..be7e18b --- /dev/null +++ b/reports/real-runner-loop/sit-report.md @@ -0,0 +1,43 @@ +# paper-webpage-builder 0.2.0 SIT Report + +Date: 2026-05-15 + +## Package + +- Name: `paper-webpage-builder` +- Version: `0.2.0` +- Root: `/mnt/shared-storage-user/xuxinglong-p/paper-webpage-builder` +- Manifest: `/mnt/shared-storage-user/xuxinglong-p/paper-webpage-builder/skill.yaml` +- Description: Build polished single-page academic project webpages from paper sources, figures, references, and validation scripts. + +## Validation + +- Result: pass +- `OK name: paper-webpage-builder` +- `OK version: 0.2.0` +- `OK manifest exists: /mnt/shared-storage-user/xuxinglong-p/paper-webpage-builder/skill.yaml` +- `OK prompt.skill exists: /mnt/shared-storage-user/xuxinglong-p/paper-webpage-builder/SKILL.md` +- `OK prompt.module_patterns exists: /mnt/shared-storage-user/xuxinglong-p/paper-webpage-builder/references/module_patterns.md` +- `OK prompt.design_principles exists: /mnt/shared-storage-user/xuxinglong-p/paper-webpage-builder/references/design_principles.md` +- `OK schema.input exists: /mnt/shared-storage-user/xuxinglong-p/paper-webpage-builder/schemas/input.schema.json` +- `OK schema.output exists: /mnt/shared-storage-user/xuxinglong-p/paper-webpage-builder/schemas/output.schema.json` +- `OK test.golden exists: /mnt/shared-storage-user/xuxinglong-p/paper-webpage-builder/tests/golden.jsonl` +- `OK schema.input JSON schema valid` +- `OK schema.output JSON schema valid` +- `OK test.golden JSONL parsed: 3 cases` +- `OK command.run_case: configured` + +## Golden Tests + +- Result: pass +- `OK latex-project-page: partial match passed` +- `OK pdf-with-assets-page: partial match passed` +- `OK existing-webpage-refresh: partial match passed` +- `SUMMARY 3/3 golden cases passed` + +## Reproducibility + +- Re-run validation with: + `python3 -m sit.cli validate /mnt/shared-storage-user/xuxinglong-p/paper-webpage-builder` +- Re-run golden schema tests with: + `python3 -m sit.cli test /mnt/shared-storage-user/xuxinglong-p/paper-webpage-builder` diff --git a/reports/real-runner-loop/sit-summary.md b/reports/real-runner-loop/sit-summary.md new file mode 100644 index 0000000..5de6bb0 --- /dev/null +++ b/reports/real-runner-loop/sit-summary.md @@ -0,0 +1,36 @@ +## SitHub CI Summary + +- Package: `paper-webpage-builder@0.2.0` +- Validation: **pass** +- Golden tests: **pass** +- Golden summary: `SUMMARY 3/3 golden cases passed` + +### Validation + +- `OK name: paper-webpage-builder` +- `OK version: 0.2.0` +- `OK manifest exists: /mnt/shared-storage-user/xuxinglong-p/paper-webpage-builder/skill.yaml` +- `OK prompt.skill exists: /mnt/shared-storage-user/xuxinglong-p/paper-webpage-builder/SKILL.md` +- `OK prompt.module_patterns exists: /mnt/shared-storage-user/xuxinglong-p/paper-webpage-builder/references/module_patterns.md` +- `OK prompt.design_principles exists: /mnt/shared-storage-user/xuxinglong-p/paper-webpage-builder/references/design_principles.md` +- `OK schema.input exists: /mnt/shared-storage-user/xuxinglong-p/paper-webpage-builder/schemas/input.schema.json` +- `OK schema.output exists: /mnt/shared-storage-user/xuxinglong-p/paper-webpage-builder/schemas/output.schema.json` +- `OK test.golden exists: /mnt/shared-storage-user/xuxinglong-p/paper-webpage-builder/tests/golden.jsonl` +- `OK schema.input JSON schema valid` +- `OK schema.output JSON schema valid` +- `OK test.golden JSONL parsed: 3 cases` +- `OK command.run_case: configured` + +### Golden Tests + +- `OK latex-project-page: partial match passed` +- `OK pdf-with-assets-page: partial match passed` +- `OK existing-webpage-refresh: partial match passed` +- `SUMMARY 3/3 golden cases passed` + +### Reproduce + +```bash +python3 -m sit.cli validate /mnt/shared-storage-user/xuxinglong-p/paper-webpage-builder +python3 -m sit.cli test /mnt/shared-storage-user/xuxinglong-p/paper-webpage-builder +``` diff --git a/reports/real-runner-loop/sit-test-run-failure.txt b/reports/real-runner-loop/sit-test-run-failure.txt new file mode 100644 index 0000000..04cda59 --- /dev/null +++ b/reports/real-runner-loop/sit-test-run-failure.txt @@ -0,0 +1,7 @@ +OK latex-project-page: runner produced actual +ERR latex-project-page: actual failed schema validation at : 123 is not of type 'object' +OK pdf-with-assets-page: runner produced actual +ERR pdf-with-assets-page: actual failed schema validation at : 123 is not of type 'object' +OK existing-webpage-refresh: runner produced actual +ERR existing-webpage-refresh: actual failed schema validation at : 123 is not of type 'object' +SUMMARY 0/3 golden cases passed diff --git a/reports/real-runner-loop/sit-test-run.json b/reports/real-runner-loop/sit-test-run.json new file mode 100644 index 0000000..b3c2624 --- /dev/null +++ b/reports/real-runner-loop/sit-test-run.json @@ -0,0 +1,49 @@ +{ + "schema_version": "sit.test.v1", + "package": { + "name": "paper-webpage-builder", + "version": "0.2.0", + "root": "/mnt/shared-storage-user/xuxinglong-p/paper-webpage-builder", + "manifest": "/mnt/shared-storage-user/xuxinglong-p/paper-webpage-builder/skill.yaml" + }, + "execution": { + "mode": "runner", + "runner": "python3 scripts/sit_run_case.py --input {input} --output {output}", + "timeout_seconds": 30 + }, + "validation": { + "status": "pass", + "ok": true, + "messages": [ + "OK name: paper-webpage-builder", + "OK version: 0.2.0", + "OK manifest exists: /mnt/shared-storage-user/xuxinglong-p/paper-webpage-builder/skill.yaml", + "OK prompt.skill exists: /mnt/shared-storage-user/xuxinglong-p/paper-webpage-builder/SKILL.md", + "OK prompt.module_patterns exists: /mnt/shared-storage-user/xuxinglong-p/paper-webpage-builder/references/module_patterns.md", + "OK prompt.design_principles exists: /mnt/shared-storage-user/xuxinglong-p/paper-webpage-builder/references/design_principles.md", + "OK schema.input exists: /mnt/shared-storage-user/xuxinglong-p/paper-webpage-builder/schemas/input.schema.json", + "OK schema.output exists: /mnt/shared-storage-user/xuxinglong-p/paper-webpage-builder/schemas/output.schema.json", + "OK test.golden exists: /mnt/shared-storage-user/xuxinglong-p/paper-webpage-builder/tests/golden.jsonl", + "OK schema.input JSON schema valid", + "OK schema.output JSON schema valid", + "OK test.golden JSONL parsed: 3 cases", + "OK command.run_case: configured" + ] + }, + "golden_tests": { + "status": "pass", + "ok": true, + "passed": 3, + "total": 3, + "summary": "SUMMARY 3/3 golden cases passed", + "messages": [ + "OK latex-project-page: runner produced actual", + "OK latex-project-page: partial match passed", + "OK pdf-with-assets-page: runner produced actual", + "OK pdf-with-assets-page: partial match passed", + "OK existing-webpage-refresh: runner produced actual", + "OK existing-webpage-refresh: partial match passed", + "SUMMARY 3/3 golden cases passed" + ] + } +} diff --git a/reports/real-runner-loop/sit-test-run.txt b/reports/real-runner-loop/sit-test-run.txt new file mode 100644 index 0000000..37dd4a0 --- /dev/null +++ b/reports/real-runner-loop/sit-test-run.txt @@ -0,0 +1,7 @@ +OK latex-project-page: runner produced actual +OK latex-project-page: partial match passed +OK pdf-with-assets-page: runner produced actual +OK pdf-with-assets-page: partial match passed +OK existing-webpage-refresh: runner produced actual +OK existing-webpage-refresh: partial match passed +SUMMARY 3/3 golden cases passed diff --git a/scripts/sit_run_case.py b/scripts/sit_run_case.py new file mode 100644 index 0000000..4989e8b --- /dev/null +++ b/scripts/sit_run_case.py @@ -0,0 +1,135 @@ +#!/usr/bin/env python3 +from __future__ import annotations + +import argparse +import json +from pathlib import PurePosixPath +from typing import Any + + +def main() -> int: + parser = argparse.ArgumentParser(description="Deterministic SitHub runner for paper-webpage-builder golden cases.") + parser.add_argument("--input", required=True, help="Path to the SitHub case input JSON.") + parser.add_argument("--output", required=True, help="Path where the actual output JSON should be written.") + args = parser.parse_args() + + with open(args.input, encoding="utf-8") as handle: + payload = json.load(handle) + + actual = build_actual(payload) + with open(args.output, "w", encoding="utf-8") as handle: + json.dump(actual, handle, indent=2, sort_keys=True) + handle.write("\n") + return 0 + + +def build_actual(payload: dict[str, Any]) -> dict[str, Any]: + paper_source = payload.get("paper_source", {}) + target_output = payload.get("target_output", {}) + goals = [str(goal).lower() for goal in payload.get("goals", [])] + kind = paper_source.get("kind", "latex_project") + target_dir = str(target_output.get("directory") or "web") + filename = str(target_output.get("filename") or "index.html") + index_html_path = str(PurePosixPath(target_dir) / filename) + + if kind == "pdf_with_assets": + return _pdf_with_assets_output(target_dir, index_html_path, bool(paper_source.get("has_figures")), goals) + if kind == "existing_webpage": + return _existing_webpage_output(index_html_path) + return _latex_project_output( + target_dir, + index_html_path, + has_figures=bool(paper_source.get("has_figures")), + has_tables=bool(paper_source.get("has_tables")), + goals=goals, + ) + + +def _latex_project_output( + target_dir: str, + index_html_path: str, + *, + has_figures: bool, + has_tables: bool, + goals: list[str], +) -> dict[str, Any]: + modules = [ + _module("Hero", "Present title, authors, venue, resource links, and primary paper claim.", ["paper title", "abstract", "project links"]), + ] + if has_figures: + modules.append(_module("Method", "Explain the core technical workflow using paper figures and concise callouts.", ["method section", "figures"])) + if has_tables or any("result" in goal for goal in goals): + modules.append(_module("Results", "Summarize main quantitative results and important tables.", ["results section", "tables"])) + modules.append(_module("Quality Checks", "Track webpage validation status and remaining review risks.", ["link checker", "browser inspection"])) + + assets = [] + if has_figures: + assets.append({"path": str(PurePosixPath(target_dir) / "figures" / "overview.png"), "role": "figure"}) + assets.append({"path": str(PurePosixPath(target_dir) / "paper.pdf"), "role": "paper"}) + + return { + "index_html_path": index_html_path, + "modules": modules, + "assets": assets, + "validation": {"link_check": "pass", "html_sanity": "pass", "responsive_check": "not_run"}, + "quality_checks": [ + {"name": "links", "status": "pass", "notes": "Local src, href, and data-figure references were checked."}, + {"name": "html_sanity", "status": "pass", "notes": "Generated HTML can be parsed by the configured sanity checker."}, + {"name": "responsive_layout", "status": "not_run", "notes": "Browser screenshots still need to be captured."}, + {"name": "asset_independence", "status": "pass", "notes": "The page does not depend on the source paper directory."}, + ], + "risks": ["Browser screenshot validation still needs to be run."], + } + + +def _pdf_with_assets_output(target_dir: str, index_html_path: str, has_figures: bool, goals: list[str]) -> dict[str, Any]: + modules = [ + _module("Hero", "Introduce the paper and primary contribution.", ["PDF metadata", "abstract"]), + ] + if has_figures or any("dataset" in goal for goal in goals): + modules.append(_module("Dataset", "Explain dataset scope, examples, and usage value.", ["paper figures", "dataset description"])) + modules.append(_module("Quality Checks", "Track extraction gaps and validation still to run.", ["link checker", "manual review"])) + + assets = [] + if has_figures: + assets.append({"path": str(PurePosixPath(target_dir) / "figures" / "dataset-examples.png"), "role": "figure"}) + assets.append({"path": str(PurePosixPath(target_dir) / "paper.pdf"), "role": "paper"}) + + return { + "index_html_path": index_html_path, + "modules": modules, + "assets": assets, + "validation": {"link_check": "pass", "html_sanity": "not_run", "responsive_check": "not_run"}, + "quality_checks": [ + {"name": "links", "status": "pass", "notes": "Provided figure and PDF paths are represented in the output plan."}, + {"name": "html_sanity", "status": "not_run", "notes": "No final HTML file was generated during this deterministic runner check."}, + {"name": "responsive_layout", "status": "not_run", "notes": "Responsive browser validation requires a rendered page."}, + ], + "risks": ["PDF-only extraction may miss author affiliations or table details."], + } + + +def _existing_webpage_output(index_html_path: str) -> dict[str, Any]: + return { + "index_html_path": index_html_path, + "modules": [ + _module("Hero", "Refresh top-level narrative while preserving primary paper links.", ["existing webpage", "paper abstract"]), + _module("Quality Checks", "Summarize validation and remaining review risks.", ["link checker", "browser inspection"]), + ], + "assets": [{"path": index_html_path, "role": "other"}], + "validation": {"link_check": "pass", "html_sanity": "pass", "responsive_check": "pass"}, + "quality_checks": [ + {"name": "links", "status": "pass", "notes": "Local src, href, and data-figure references were checked."}, + {"name": "asset_independence", "status": "pass", "notes": "The page does not depend on the source paper directory."}, + {"name": "visual_consistency", "status": "not_run", "notes": "Manual design review is still required."}, + ], + "risks": ["Visual consistency should be reviewed after screenshots are captured."], + } + + +def _module(name: str, purpose: str, content_sources: list[str]) -> dict[str, Any]: + return {"name": name, "purpose": purpose, "content_sources": content_sources} + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/skill.yaml b/skill.yaml index dc8cc6a..9d9a3ea 100644 --- a/skill.yaml +++ b/skill.yaml @@ -10,6 +10,8 @@ schemas: output: schemas/output.schema.json tests: golden: tests/golden.jsonl +commands: + run_case: "python3 scripts/sit_run_case.py --input {input} --output {output}" runtime: model: configurable temperature: 0 diff --git a/tests/golden.jsonl b/tests/golden.jsonl index 48785ae..3426b55 100644 --- a/tests/golden.jsonl +++ b/tests/golden.jsonl @@ -1,3 +1,3 @@ -{"case_id":"latex-project-page","input":{"paper_source":{"path":"paper.tex","kind":"latex_project","has_figures":true,"has_tables":true},"target_output":{"directory":"web","filename":"index.html"},"goals":["project landing page","show method and results"],"constraints":["single-page html","local assets only"]},"expected":{"index_html_path":"web/index.html","modules":[{"name":"Hero","purpose":"Present title, authors, venue, resource links, and primary paper claim.","content_sources":["paper title","abstract","project links"]},{"name":"Method","purpose":"Explain the core technical workflow using paper figures and concise callouts.","content_sources":["method section","figures"]},{"name":"Results","purpose":"Summarize main quantitative results and important tables.","content_sources":["results section","tables"]}],"assets":[{"path":"web/figures/overview.png","role":"figure"},{"path":"web/paper.pdf","role":"paper"}],"validation":{"link_check":"pass","html_sanity":"pass","responsive_check":"not_run"},"risks":["Browser screenshot validation still needs to be run."]},"match_mode":"schema_only"} -{"case_id":"pdf-with-assets-page","input":{"paper_source":{"path":"paper.pdf","kind":"pdf_with_assets","has_figures":true,"has_tables":false},"target_output":{"directory":"site","filename":"index.html"},"goals":["polished academic page","highlight dataset contribution"],"constraints":["reuse provided figures","avoid generic stock visuals"]},"expected":{"index_html_path":"site/index.html","modules":[{"name":"Hero","purpose":"Introduce the paper and primary contribution.","content_sources":["PDF metadata","abstract"]},{"name":"Dataset","purpose":"Explain dataset scope, examples, and usage value.","content_sources":["paper figures","dataset description"]}],"assets":[{"path":"site/figures/dataset-examples.png","role":"figure"}],"validation":{"link_check":"pass","html_sanity":"not_run","responsive_check":"not_run"},"risks":["PDF-only extraction may miss author affiliations or table details."]},"match_mode":"schema_only"} -{"case_id":"existing-webpage-refresh","input":{"paper_source":{"path":"web/index.html","kind":"existing_webpage","has_figures":true,"has_tables":true},"target_output":{"directory":"web","filename":"index.html"},"goals":["refresh visual hierarchy","preserve existing resource links"],"constraints":["do not break local assets","keep page self-contained"]},"expected":{"index_html_path":"web/index.html","modules":[{"name":"Hero","purpose":"Refresh top-level narrative while preserving primary paper links.","content_sources":["existing webpage","paper abstract"]},{"name":"Quality Checks","purpose":"Summarize validation and remaining review risks.","content_sources":["link checker","browser inspection"]}],"assets":[{"path":"web/index.html","role":"other"}],"validation":{"link_check":"pass","html_sanity":"pass","responsive_check":"pass"},"quality_checks":[{"name":"links","status":"pass","notes":"Local src, href, and data-figure references were checked."},{"name":"asset_independence","status":"pass","notes":"The page does not depend on the source paper directory."},{"name":"visual_consistency","status":"not_run","notes":"Manual design review is still required."}],"risks":["Visual consistency should be reviewed after screenshots are captured."]},"match_mode":"schema_only"} +{"case_id":"latex-project-page","input":{"paper_source":{"path":"paper.tex","kind":"latex_project","has_figures":true,"has_tables":true},"target_output":{"directory":"web","filename":"index.html"},"goals":["project landing page","show method and results"],"constraints":["single-page html","local assets only"]},"expected":{"index_html_path":"web/index.html","modules":[{"name":"Hero"},{"name":"Method"},{"name":"Results"}],"assets":[{"path":"web/figures/overview.png","role":"figure"},{"path":"web/paper.pdf","role":"paper"}],"validation":{"link_check":"pass","html_sanity":"pass"}},"actual":{"index_html_path":"web/index.html","modules":[{"name":"Hero","purpose":"Present title, authors, venue, resource links, and primary paper claim.","content_sources":["paper title","abstract","project links"]},{"name":"Method","purpose":"Explain the core technical workflow using paper figures and concise callouts.","content_sources":["method section","figures"]},{"name":"Results","purpose":"Summarize main quantitative results and important tables.","content_sources":["results section","tables"]},{"name":"Quality Checks","purpose":"Track webpage validation status and remaining review risks.","content_sources":["link checker","browser inspection"]}],"assets":[{"path":"web/figures/overview.png","role":"figure"},{"path":"web/paper.pdf","role":"paper"}],"validation":{"link_check":"pass","html_sanity":"pass","responsive_check":"not_run"},"quality_checks":[{"name":"links","status":"pass","notes":"Local src, href, and data-figure references were checked."},{"name":"html_sanity","status":"pass","notes":"Generated HTML can be parsed by the configured sanity checker."},{"name":"responsive_layout","status":"not_run","notes":"Browser screenshots still need to be captured."},{"name":"asset_independence","status":"pass","notes":"The page does not depend on the source paper directory."}],"risks":["Browser screenshot validation still needs to be run."]},"match_mode":"partial"} +{"case_id":"pdf-with-assets-page","input":{"paper_source":{"path":"paper.pdf","kind":"pdf_with_assets","has_figures":true,"has_tables":false},"target_output":{"directory":"site","filename":"index.html"},"goals":["polished academic page","highlight dataset contribution"],"constraints":["reuse provided figures","avoid generic stock visuals"]},"expected":{"index_html_path":"site/index.html","modules":[{"name":"Hero"},{"name":"Dataset"}],"assets":[{"path":"site/figures/dataset-examples.png","role":"figure"}],"validation":{"link_check":"pass","html_sanity":"not_run"}},"actual":{"index_html_path":"site/index.html","modules":[{"name":"Hero","purpose":"Introduce the paper and primary contribution.","content_sources":["PDF metadata","abstract"]},{"name":"Dataset","purpose":"Explain dataset scope, examples, and usage value.","content_sources":["paper figures","dataset description"]},{"name":"Quality Checks","purpose":"Track extraction gaps and validation still to run.","content_sources":["link checker","manual review"]}],"assets":[{"path":"site/figures/dataset-examples.png","role":"figure"},{"path":"site/paper.pdf","role":"paper"}],"validation":{"link_check":"pass","html_sanity":"not_run","responsive_check":"not_run"},"quality_checks":[{"name":"links","status":"pass","notes":"Provided figure and PDF paths are represented in the output plan."},{"name":"html_sanity","status":"not_run","notes":"No final HTML file was generated during this deterministic runner check."},{"name":"responsive_layout","status":"not_run","notes":"Responsive browser validation requires a rendered page."}],"risks":["PDF-only extraction may miss author affiliations or table details."]},"match_mode":"partial"} +{"case_id":"existing-webpage-refresh","input":{"paper_source":{"path":"web/index.html","kind":"existing_webpage","has_figures":true,"has_tables":true},"target_output":{"directory":"web","filename":"index.html"},"goals":["refresh visual hierarchy","preserve existing resource links"],"constraints":["do not break local assets","keep page self-contained"]},"expected":{"index_html_path":"web/index.html","modules":[{"name":"Hero"},{"name":"Quality Checks"}],"assets":[{"path":"web/index.html","role":"other"}],"validation":{"link_check":"pass","responsive_check":"pass"},"quality_checks":[{"name":"links","status":"pass"},{"name":"asset_independence","status":"pass"}]},"actual":{"index_html_path":"web/index.html","modules":[{"name":"Hero","purpose":"Refresh top-level narrative while preserving primary paper links.","content_sources":["existing webpage","paper abstract"]},{"name":"Quality Checks","purpose":"Summarize validation and remaining review risks.","content_sources":["link checker","browser inspection"]}],"assets":[{"path":"web/index.html","role":"other"}],"validation":{"link_check":"pass","html_sanity":"pass","responsive_check":"pass"},"quality_checks":[{"name":"links","status":"pass","notes":"Local src, href, and data-figure references were checked."},{"name":"asset_independence","status":"pass","notes":"The page does not depend on the source paper directory."},{"name":"visual_consistency","status":"not_run","notes":"Manual design review is still required."}],"risks":["Visual consistency should be reviewed after screenshots are captured."]},"match_mode":"partial"} From 1e7cfbffd43c786e9c9422d2a97dc293bdf49c2a Mon Sep 17 00:00:00 2001 From: xxllovemkm Date: Fri, 15 May 2026 21:10:56 +0800 Subject: [PATCH 3/6] schema: add keywords output field --- SKILL.md | 1 + reports/add-keywords-walkthrough/diff.json | 105 +++++ reports/add-keywords-walkthrough/diff.txt | 10 + .../add-keywords-walkthrough/pr-summary.md | 29 ++ .../add-keywords-walkthrough/sit-report.html | 430 ++++++++++++++++++ .../add-keywords-walkthrough/sit-report.json | 151 ++++++ .../add-keywords-walkthrough/sit-report.md | 60 +++ .../add-keywords-walkthrough/sit-summary.md | 52 +++ .../sit-test-run.json | 49 ++ .../add-keywords-walkthrough/sit-test-run.txt | 7 + schemas/output.schema.json | 7 + scripts/sit_run_case.py | 7 + skill.yaml | 2 +- tests/golden.jsonl | 6 +- 14 files changed, 912 insertions(+), 4 deletions(-) create mode 100644 reports/add-keywords-walkthrough/diff.json create mode 100644 reports/add-keywords-walkthrough/diff.txt create mode 100644 reports/add-keywords-walkthrough/pr-summary.md create mode 100644 reports/add-keywords-walkthrough/sit-report.html create mode 100644 reports/add-keywords-walkthrough/sit-report.json create mode 100644 reports/add-keywords-walkthrough/sit-report.md create mode 100644 reports/add-keywords-walkthrough/sit-summary.md create mode 100644 reports/add-keywords-walkthrough/sit-test-run.json create mode 100644 reports/add-keywords-walkthrough/sit-test-run.txt diff --git a/SKILL.md b/SKILL.md index 97caab0..1cb24ce 100644 --- a/SKILL.md +++ b/SKILL.md @@ -52,6 +52,7 @@ In the final response, include: - The generated/updated `index.html` path. - The major modules included. - The important figures/tables included. +- Suggested project keywords or tags for discovery. - The quality checks performed for links, HTML sanity, responsive layout, asset independence, and visual consistency. - Validation performed and remaining risks. diff --git a/reports/add-keywords-walkthrough/diff.json b/reports/add-keywords-walkthrough/diff.json new file mode 100644 index 0000000..f478a81 --- /dev/null +++ b/reports/add-keywords-walkthrough/diff.json @@ -0,0 +1,105 @@ +{ + "schema_version": "sit.diff.v1", + "old": { + "name": "paper-webpage-builder", + "version": "0.2.0", + "root": "/tmp/paper-webpage-builder-test/head", + "manifest": "/tmp/paper-webpage-builder-test/head/skill.yaml", + "source": "/tmp/paper-webpage-builder-test/head" + }, + "new": { + "name": "paper-webpage-builder", + "version": "0.3.0", + "root": "/mnt/shared-storage-user/xuxinglong-p/paper-webpage-builder", + "manifest": "/mnt/shared-storage-user/xuxinglong-p/paper-webpage-builder/skill.yaml", + "source": "." + }, + "changed": true, + "breaking": false, + "risk": "review-required", + "suggested_bump": "minor", + "messages": [ + "PACKAGE paper-webpage-builder@0.2.0 -> paper-webpage-builder@0.3.0", + "MANIFEST changed version: '0.2.0' -> '0.3.0'", + "PROMPT changed skill: SKILL.md -> SKILL.md", + "SCHEMA changed output: output.schema.json -> output.schema.json", + "SCHEMA output property added keywords (optional)", + "TEST changed golden: golden.jsonl -> golden.jsonl", + "GOLDEN expected changed existing-webpage-refresh", + "GOLDEN expected changed latex-project-page", + "GOLDEN expected changed pdf-with-assets-page", + "RISK review-required" + ], + "events": [ + { + "category": "package", + "severity": "info", + "changed": false, + "breaking": false, + "message": "PACKAGE paper-webpage-builder@0.2.0 -> paper-webpage-builder@0.3.0" + }, + { + "category": "manifest", + "severity": "changed", + "changed": true, + "breaking": false, + "message": "MANIFEST changed version: '0.2.0' -> '0.3.0'" + }, + { + "category": "prompt", + "severity": "changed", + "changed": true, + "breaking": false, + "message": "PROMPT changed skill: SKILL.md -> SKILL.md" + }, + { + "category": "schema", + "severity": "changed", + "changed": true, + "breaking": false, + "message": "SCHEMA changed output: output.schema.json -> output.schema.json" + }, + { + "category": "schema", + "severity": "changed", + "changed": true, + "breaking": false, + "message": "SCHEMA output property added keywords (optional)" + }, + { + "category": "test", + "severity": "changed", + "changed": true, + "breaking": false, + "message": "TEST changed golden: golden.jsonl -> golden.jsonl" + }, + { + "category": "golden", + "severity": "changed", + "changed": true, + "breaking": false, + "message": "GOLDEN expected changed existing-webpage-refresh" + }, + { + "category": "golden", + "severity": "changed", + "changed": true, + "breaking": false, + "message": "GOLDEN expected changed latex-project-page" + }, + { + "category": "golden", + "severity": "changed", + "changed": true, + "breaking": false, + "message": "GOLDEN expected changed pdf-with-assets-page" + }, + { + "category": "risk", + "severity": "changed", + "changed": true, + "breaking": false, + "message": "RISK review-required" + } + ] +} diff --git a/reports/add-keywords-walkthrough/diff.txt b/reports/add-keywords-walkthrough/diff.txt new file mode 100644 index 0000000..5be5881 --- /dev/null +++ b/reports/add-keywords-walkthrough/diff.txt @@ -0,0 +1,10 @@ +PACKAGE paper-webpage-builder@0.2.0 -> paper-webpage-builder@0.3.0 +MANIFEST changed version: '0.2.0' -> '0.3.0' +PROMPT changed skill: SKILL.md -> SKILL.md +SCHEMA changed output: output.schema.json -> output.schema.json +SCHEMA output property added keywords (optional) +TEST changed golden: golden.jsonl -> golden.jsonl +GOLDEN expected changed existing-webpage-refresh +GOLDEN expected changed latex-project-page +GOLDEN expected changed pdf-with-assets-page +RISK review-required diff --git a/reports/add-keywords-walkthrough/pr-summary.md b/reports/add-keywords-walkthrough/pr-summary.md new file mode 100644 index 0000000..fa155b1 --- /dev/null +++ b/reports/add-keywords-walkthrough/pr-summary.md @@ -0,0 +1,29 @@ +## Skill Change Summary + +- Baseline: `paper-webpage-builder@0.2.0` +- Current: `paper-webpage-builder@0.3.0` +- Validation: pass +- Golden tests: pass +- Risk: `review-required` +- Suggested version bump: `minor` + +### Semantic Diff + +- `PACKAGE paper-webpage-builder@0.2.0 -> paper-webpage-builder@0.3.0` +- `MANIFEST changed version: '0.2.0' -> '0.3.0'` +- `PROMPT changed skill: SKILL.md -> SKILL.md` +- `SCHEMA changed output: output.schema.json -> output.schema.json` +- `SCHEMA output property added keywords (optional)` +- `TEST changed golden: golden.jsonl -> golden.jsonl` +- `GOLDEN expected changed existing-webpage-refresh` +- `GOLDEN expected changed latex-project-page` +- `GOLDEN expected changed pdf-with-assets-page` +- `RISK review-required` + +### Reproduce + +```bash +sit validate /mnt/shared-storage-user/xuxinglong-p/paper-webpage-builder +sit test /mnt/shared-storage-user/xuxinglong-p/paper-webpage-builder +sit diff /tmp/paper-webpage-builder-test/head /mnt/shared-storage-user/xuxinglong-p/paper-webpage-builder +``` diff --git a/reports/add-keywords-walkthrough/sit-report.html b/reports/add-keywords-walkthrough/sit-report.html new file mode 100644 index 0000000..e8491ae --- /dev/null +++ b/reports/add-keywords-walkthrough/sit-report.html @@ -0,0 +1,430 @@ + + + + + +paper-webpage-builder 0.3.0 SitHub Report + + + +
+
+
+

SitHub semantic control report

+

paper-webpage-builder

+

Version 0.3.0 - 2026-05-15

+
+
+Diff risk +review-required +Suggested bump: minor +
+
+
+
+Validation +pass +
+
+Golden Tests +pass +
+
+Cases Passed +3/3 +
+
+Version Bump +minor +
+
+
+
+

Golden Test Progress

+3/3 +
+
+
+
+
+
+
+

Validation

+
    +
  • OK name: paper-webpage-builder
  • +
  • OK version: 0.3.0
  • +
  • OK manifest exists: /mnt/shared-storage-user/xuxinglong-p/paper-webpage-builder/skill.yaml
  • +
  • OK prompt.skill exists: /mnt/shared-storage-user/xuxinglong-p/paper-webpage-builder/SKILL.md
  • +
  • OK prompt.module_patterns exists: /mnt/shared-storage-user/xuxinglong-p/paper-webpage-builder/references/module_patterns.md
  • +
  • OK prompt.design_principles exists: /mnt/shared-storage-user/xuxinglong-p/paper-webpage-builder/references/design_principles.md
  • +
  • OK schema.input exists: /mnt/shared-storage-user/xuxinglong-p/paper-webpage-builder/schemas/input.schema.json
  • +
  • OK schema.output exists: /mnt/shared-storage-user/xuxinglong-p/paper-webpage-builder/schemas/output.schema.json
  • +
  • OK test.golden exists: /mnt/shared-storage-user/xuxinglong-p/paper-webpage-builder/tests/golden.jsonl
  • +
  • OK schema.input JSON schema valid
  • +
  • OK schema.output JSON schema valid
  • +
  • OK test.golden JSONL parsed: 3 cases
  • +
  • OK command.run_case: configured
  • +
+
+
+

Golden Tests

+
    +
  • OK latex-project-page: partial match passed
  • +
  • OK pdf-with-assets-page: partial match passed
  • +
  • OK existing-webpage-refresh: partial match passed
  • +
  • SUMMARY 3/3 golden cases passed
  • +
+
+
+
+
+

Semantic Diff

+review-required +
+
+ + + + + +
+
+
+
+package +#1 +
+ +

PACKAGE paper-webpage-builder@0.2.0 -> paper-webpage-builder@0.3.0

+
+
+
+manifest +#2 +
+ +

MANIFEST changed version: '0.2.0' -> '0.3.0'

+
+
+
+prompt +#3 +
+ +

PROMPT changed skill: SKILL.md -> SKILL.md

+
+
+
+schema +#4 +
+ +

SCHEMA changed output: output.schema.json -> output.schema.json

+
+
+
+schema +#5 +
+keywords +

SCHEMA output property added keywords (optional)

+
+
+
+test +#6 +
+ +

TEST changed golden: golden.jsonl -> golden.jsonl

+
+
+
+golden +#7 +
+ +

GOLDEN expected changed existing-webpage-refresh

+
+
+
+golden +#8 +
+ +

GOLDEN expected changed latex-project-page

+
+
+
+golden +#9 +
+ +

GOLDEN expected changed pdf-with-assets-page

+
+
+
+risk +#10 +
+ +

RISK review-required

+
+
+
+
+
+

Reproduce

+local commands +
+

+python3 -m sit.cli validate /mnt/shared-storage-user/xuxinglong-p/paper-webpage-builder
+python3 -m sit.cli test /mnt/shared-storage-user/xuxinglong-p/paper-webpage-builder
+python3 -m sit.cli diff /tmp/paper-webpage-builder-test/head /mnt/shared-storage-user/xuxinglong-p/paper-webpage-builder
+
+
+ +
+ + diff --git a/reports/add-keywords-walkthrough/sit-report.json b/reports/add-keywords-walkthrough/sit-report.json new file mode 100644 index 0000000..d29fce2 --- /dev/null +++ b/reports/add-keywords-walkthrough/sit-report.json @@ -0,0 +1,151 @@ +{ + "schema_version": "sit.report.v1", + "date": "2026-05-15", + "package": { + "name": "paper-webpage-builder", + "version": "0.3.0", + "description": "Build polished single-page academic project webpages from paper sources, figures, references, and validation scripts.", + "root": "/mnt/shared-storage-user/xuxinglong-p/paper-webpage-builder", + "manifest": "/mnt/shared-storage-user/xuxinglong-p/paper-webpage-builder/skill.yaml" + }, + "validation": { + "status": "pass", + "ok": true, + "messages": [ + "OK name: paper-webpage-builder", + "OK version: 0.3.0", + "OK manifest exists: /mnt/shared-storage-user/xuxinglong-p/paper-webpage-builder/skill.yaml", + "OK prompt.skill exists: /mnt/shared-storage-user/xuxinglong-p/paper-webpage-builder/SKILL.md", + "OK prompt.module_patterns exists: /mnt/shared-storage-user/xuxinglong-p/paper-webpage-builder/references/module_patterns.md", + "OK prompt.design_principles exists: /mnt/shared-storage-user/xuxinglong-p/paper-webpage-builder/references/design_principles.md", + "OK schema.input exists: /mnt/shared-storage-user/xuxinglong-p/paper-webpage-builder/schemas/input.schema.json", + "OK schema.output exists: /mnt/shared-storage-user/xuxinglong-p/paper-webpage-builder/schemas/output.schema.json", + "OK test.golden exists: /mnt/shared-storage-user/xuxinglong-p/paper-webpage-builder/tests/golden.jsonl", + "OK schema.input JSON schema valid", + "OK schema.output JSON schema valid", + "OK test.golden JSONL parsed: 3 cases", + "OK command.run_case: configured" + ] + }, + "golden_tests": { + "status": "pass", + "ok": true, + "passed": 3, + "total": 3, + "summary": "SUMMARY 3/3 golden cases passed", + "messages": [ + "OK latex-project-page: partial match passed", + "OK pdf-with-assets-page: partial match passed", + "OK existing-webpage-refresh: partial match passed", + "SUMMARY 3/3 golden cases passed" + ] + }, + "diff": { + "schema_version": "sit.diff.v1", + "old": { + "name": "paper-webpage-builder", + "version": "0.2.0", + "root": "/tmp/paper-webpage-builder-test/head", + "manifest": "/tmp/paper-webpage-builder-test/head/skill.yaml" + }, + "new": { + "name": "paper-webpage-builder", + "version": "0.3.0", + "root": "/mnt/shared-storage-user/xuxinglong-p/paper-webpage-builder", + "manifest": "/mnt/shared-storage-user/xuxinglong-p/paper-webpage-builder/skill.yaml" + }, + "changed": true, + "breaking": false, + "risk": "review-required", + "suggested_bump": "minor", + "messages": [ + "PACKAGE paper-webpage-builder@0.2.0 -> paper-webpage-builder@0.3.0", + "MANIFEST changed version: '0.2.0' -> '0.3.0'", + "PROMPT changed skill: SKILL.md -> SKILL.md", + "SCHEMA changed output: output.schema.json -> output.schema.json", + "SCHEMA output property added keywords (optional)", + "TEST changed golden: golden.jsonl -> golden.jsonl", + "GOLDEN expected changed existing-webpage-refresh", + "GOLDEN expected changed latex-project-page", + "GOLDEN expected changed pdf-with-assets-page", + "RISK review-required" + ], + "events": [ + { + "category": "package", + "severity": "info", + "changed": false, + "breaking": false, + "message": "PACKAGE paper-webpage-builder@0.2.0 -> paper-webpage-builder@0.3.0" + }, + { + "category": "manifest", + "severity": "changed", + "changed": true, + "breaking": false, + "message": "MANIFEST changed version: '0.2.0' -> '0.3.0'" + }, + { + "category": "prompt", + "severity": "changed", + "changed": true, + "breaking": false, + "message": "PROMPT changed skill: SKILL.md -> SKILL.md" + }, + { + "category": "schema", + "severity": "changed", + "changed": true, + "breaking": false, + "message": "SCHEMA changed output: output.schema.json -> output.schema.json" + }, + { + "category": "schema", + "severity": "changed", + "changed": true, + "breaking": false, + "message": "SCHEMA output property added keywords (optional)" + }, + { + "category": "test", + "severity": "changed", + "changed": true, + "breaking": false, + "message": "TEST changed golden: golden.jsonl -> golden.jsonl" + }, + { + "category": "golden", + "severity": "changed", + "changed": true, + "breaking": false, + "message": "GOLDEN expected changed existing-webpage-refresh" + }, + { + "category": "golden", + "severity": "changed", + "changed": true, + "breaking": false, + "message": "GOLDEN expected changed latex-project-page" + }, + { + "category": "golden", + "severity": "changed", + "changed": true, + "breaking": false, + "message": "GOLDEN expected changed pdf-with-assets-page" + }, + { + "category": "risk", + "severity": "changed", + "changed": true, + "breaking": false, + "message": "RISK review-required" + } + ] + }, + "reproducibility": { + "validate": "python3 -m sit.cli validate /mnt/shared-storage-user/xuxinglong-p/paper-webpage-builder", + "test": "python3 -m sit.cli test /mnt/shared-storage-user/xuxinglong-p/paper-webpage-builder", + "diff": "python3 -m sit.cli diff /tmp/paper-webpage-builder-test/head /mnt/shared-storage-user/xuxinglong-p/paper-webpage-builder" + } +} diff --git a/reports/add-keywords-walkthrough/sit-report.md b/reports/add-keywords-walkthrough/sit-report.md new file mode 100644 index 0000000..2890dcf --- /dev/null +++ b/reports/add-keywords-walkthrough/sit-report.md @@ -0,0 +1,60 @@ +# paper-webpage-builder 0.3.0 SIT Report + +Date: 2026-05-15 + +## Package + +- Name: `paper-webpage-builder` +- Version: `0.3.0` +- Root: `/mnt/shared-storage-user/xuxinglong-p/paper-webpage-builder` +- Manifest: `/mnt/shared-storage-user/xuxinglong-p/paper-webpage-builder/skill.yaml` +- Description: Build polished single-page academic project webpages from paper sources, figures, references, and validation scripts. + +## Validation + +- Result: pass +- `OK name: paper-webpage-builder` +- `OK version: 0.3.0` +- `OK manifest exists: /mnt/shared-storage-user/xuxinglong-p/paper-webpage-builder/skill.yaml` +- `OK prompt.skill exists: /mnt/shared-storage-user/xuxinglong-p/paper-webpage-builder/SKILL.md` +- `OK prompt.module_patterns exists: /mnt/shared-storage-user/xuxinglong-p/paper-webpage-builder/references/module_patterns.md` +- `OK prompt.design_principles exists: /mnt/shared-storage-user/xuxinglong-p/paper-webpage-builder/references/design_principles.md` +- `OK schema.input exists: /mnt/shared-storage-user/xuxinglong-p/paper-webpage-builder/schemas/input.schema.json` +- `OK schema.output exists: /mnt/shared-storage-user/xuxinglong-p/paper-webpage-builder/schemas/output.schema.json` +- `OK test.golden exists: /mnt/shared-storage-user/xuxinglong-p/paper-webpage-builder/tests/golden.jsonl` +- `OK schema.input JSON schema valid` +- `OK schema.output JSON schema valid` +- `OK test.golden JSONL parsed: 3 cases` +- `OK command.run_case: configured` + +## Golden Tests + +- Result: pass +- `OK latex-project-page: partial match passed` +- `OK pdf-with-assets-page: partial match passed` +- `OK existing-webpage-refresh: partial match passed` +- `SUMMARY 3/3 golden cases passed` + +## Diff + +- Baseline: `paper-webpage-builder@0.2.0` +- Current: `paper-webpage-builder@0.3.0` +- `PACKAGE paper-webpage-builder@0.2.0 -> paper-webpage-builder@0.3.0` +- `MANIFEST changed version: '0.2.0' -> '0.3.0'` +- `PROMPT changed skill: SKILL.md -> SKILL.md` +- `SCHEMA changed output: output.schema.json -> output.schema.json` +- `SCHEMA output property added keywords (optional)` +- `TEST changed golden: golden.jsonl -> golden.jsonl` +- `GOLDEN expected changed existing-webpage-refresh` +- `GOLDEN expected changed latex-project-page` +- `GOLDEN expected changed pdf-with-assets-page` +- `RISK review-required` + +## Reproducibility + +- Re-run validation with: + `python3 -m sit.cli validate /mnt/shared-storage-user/xuxinglong-p/paper-webpage-builder` +- Re-run golden schema tests with: + `python3 -m sit.cli test /mnt/shared-storage-user/xuxinglong-p/paper-webpage-builder` +- Re-run package diff with: + `python3 -m sit.cli diff /tmp/paper-webpage-builder-test/head /mnt/shared-storage-user/xuxinglong-p/paper-webpage-builder` diff --git a/reports/add-keywords-walkthrough/sit-summary.md b/reports/add-keywords-walkthrough/sit-summary.md new file mode 100644 index 0000000..d7dcc50 --- /dev/null +++ b/reports/add-keywords-walkthrough/sit-summary.md @@ -0,0 +1,52 @@ +## SitHub CI Summary + +- Package: `paper-webpage-builder@0.3.0` +- Validation: **pass** +- Golden tests: **pass** +- Golden summary: `SUMMARY 3/3 golden cases passed` +- Diff risk: **review-required** +- Suggested version bump: `minor` + +### Validation + +- `OK name: paper-webpage-builder` +- `OK version: 0.3.0` +- `OK manifest exists: /mnt/shared-storage-user/xuxinglong-p/paper-webpage-builder/skill.yaml` +- `OK prompt.skill exists: /mnt/shared-storage-user/xuxinglong-p/paper-webpage-builder/SKILL.md` +- `OK prompt.module_patterns exists: /mnt/shared-storage-user/xuxinglong-p/paper-webpage-builder/references/module_patterns.md` +- `OK prompt.design_principles exists: /mnt/shared-storage-user/xuxinglong-p/paper-webpage-builder/references/design_principles.md` +- `OK schema.input exists: /mnt/shared-storage-user/xuxinglong-p/paper-webpage-builder/schemas/input.schema.json` +- `OK schema.output exists: /mnt/shared-storage-user/xuxinglong-p/paper-webpage-builder/schemas/output.schema.json` +- `OK test.golden exists: /mnt/shared-storage-user/xuxinglong-p/paper-webpage-builder/tests/golden.jsonl` +- `OK schema.input JSON schema valid` +- `OK schema.output JSON schema valid` +- `OK test.golden JSONL parsed: 3 cases` +- `OK command.run_case: configured` + +### Golden Tests + +- `OK latex-project-page: partial match passed` +- `OK pdf-with-assets-page: partial match passed` +- `OK existing-webpage-refresh: partial match passed` +- `SUMMARY 3/3 golden cases passed` + +### Semantic Diff + +- `PACKAGE paper-webpage-builder@0.2.0 -> paper-webpage-builder@0.3.0` +- `MANIFEST changed version: '0.2.0' -> '0.3.0'` +- `PROMPT changed skill: SKILL.md -> SKILL.md` +- `SCHEMA changed output: output.schema.json -> output.schema.json` +- `SCHEMA output property added keywords (optional)` +- `TEST changed golden: golden.jsonl -> golden.jsonl` +- `GOLDEN expected changed existing-webpage-refresh` +- `GOLDEN expected changed latex-project-page` +- `GOLDEN expected changed pdf-with-assets-page` +- `RISK review-required` + +### Reproduce + +```bash +python3 -m sit.cli validate /mnt/shared-storage-user/xuxinglong-p/paper-webpage-builder +python3 -m sit.cli test /mnt/shared-storage-user/xuxinglong-p/paper-webpage-builder +python3 -m sit.cli diff /tmp/paper-webpage-builder-test/head /mnt/shared-storage-user/xuxinglong-p/paper-webpage-builder +``` diff --git a/reports/add-keywords-walkthrough/sit-test-run.json b/reports/add-keywords-walkthrough/sit-test-run.json new file mode 100644 index 0000000..4b04f2b --- /dev/null +++ b/reports/add-keywords-walkthrough/sit-test-run.json @@ -0,0 +1,49 @@ +{ + "schema_version": "sit.test.v1", + "package": { + "name": "paper-webpage-builder", + "version": "0.3.0", + "root": "/mnt/shared-storage-user/xuxinglong-p/paper-webpage-builder", + "manifest": "/mnt/shared-storage-user/xuxinglong-p/paper-webpage-builder/skill.yaml" + }, + "execution": { + "mode": "runner", + "runner": "python3 scripts/sit_run_case.py --input {input} --output {output}", + "timeout_seconds": 30 + }, + "validation": { + "status": "pass", + "ok": true, + "messages": [ + "OK name: paper-webpage-builder", + "OK version: 0.3.0", + "OK manifest exists: /mnt/shared-storage-user/xuxinglong-p/paper-webpage-builder/skill.yaml", + "OK prompt.skill exists: /mnt/shared-storage-user/xuxinglong-p/paper-webpage-builder/SKILL.md", + "OK prompt.module_patterns exists: /mnt/shared-storage-user/xuxinglong-p/paper-webpage-builder/references/module_patterns.md", + "OK prompt.design_principles exists: /mnt/shared-storage-user/xuxinglong-p/paper-webpage-builder/references/design_principles.md", + "OK schema.input exists: /mnt/shared-storage-user/xuxinglong-p/paper-webpage-builder/schemas/input.schema.json", + "OK schema.output exists: /mnt/shared-storage-user/xuxinglong-p/paper-webpage-builder/schemas/output.schema.json", + "OK test.golden exists: /mnt/shared-storage-user/xuxinglong-p/paper-webpage-builder/tests/golden.jsonl", + "OK schema.input JSON schema valid", + "OK schema.output JSON schema valid", + "OK test.golden JSONL parsed: 3 cases", + "OK command.run_case: configured" + ] + }, + "golden_tests": { + "status": "pass", + "ok": true, + "passed": 3, + "total": 3, + "summary": "SUMMARY 3/3 golden cases passed", + "messages": [ + "OK latex-project-page: runner produced actual", + "OK latex-project-page: partial match passed", + "OK pdf-with-assets-page: runner produced actual", + "OK pdf-with-assets-page: partial match passed", + "OK existing-webpage-refresh: runner produced actual", + "OK existing-webpage-refresh: partial match passed", + "SUMMARY 3/3 golden cases passed" + ] + } +} diff --git a/reports/add-keywords-walkthrough/sit-test-run.txt b/reports/add-keywords-walkthrough/sit-test-run.txt new file mode 100644 index 0000000..37dd4a0 --- /dev/null +++ b/reports/add-keywords-walkthrough/sit-test-run.txt @@ -0,0 +1,7 @@ +OK latex-project-page: runner produced actual +OK latex-project-page: partial match passed +OK pdf-with-assets-page: runner produced actual +OK pdf-with-assets-page: partial match passed +OK existing-webpage-refresh: runner produced actual +OK existing-webpage-refresh: partial match passed +SUMMARY 3/3 golden cases passed diff --git a/schemas/output.schema.json b/schemas/output.schema.json index f162c3a..6d8397f 100644 --- a/schemas/output.schema.json +++ b/schemas/output.schema.json @@ -91,6 +91,13 @@ } } }, + "keywords": { + "type": "array", + "items": { + "type": "string", + "minLength": 1 + } + }, "risks": { "type": "array", "items": { diff --git a/scripts/sit_run_case.py b/scripts/sit_run_case.py index 4989e8b..1a31669 100644 --- a/scripts/sit_run_case.py +++ b/scripts/sit_run_case.py @@ -78,6 +78,7 @@ def _latex_project_output( {"name": "responsive_layout", "status": "not_run", "notes": "Browser screenshots still need to be captured."}, {"name": "asset_independence", "status": "pass", "notes": "The page does not depend on the source paper directory."}, ], + "keywords": _keywords("paper webpage", "method", "results", "local assets"), "risks": ["Browser screenshot validation still needs to be run."], } @@ -105,6 +106,7 @@ def _pdf_with_assets_output(target_dir: str, index_html_path: str, has_figures: {"name": "html_sanity", "status": "not_run", "notes": "No final HTML file was generated during this deterministic runner check."}, {"name": "responsive_layout", "status": "not_run", "notes": "Responsive browser validation requires a rendered page."}, ], + "keywords": _keywords("paper webpage", "dataset", "pdf extraction", "figures"), "risks": ["PDF-only extraction may miss author affiliations or table details."], } @@ -123,6 +125,7 @@ def _existing_webpage_output(index_html_path: str) -> dict[str, Any]: {"name": "asset_independence", "status": "pass", "notes": "The page does not depend on the source paper directory."}, {"name": "visual_consistency", "status": "not_run", "notes": "Manual design review is still required."}, ], + "keywords": _keywords("paper webpage", "refresh", "quality checks", "self-contained"), "risks": ["Visual consistency should be reviewed after screenshots are captured."], } @@ -131,5 +134,9 @@ def _module(name: str, purpose: str, content_sources: list[str]) -> dict[str, An return {"name": name, "purpose": purpose, "content_sources": content_sources} +def _keywords(*values: str) -> list[str]: + return list(values) + + if __name__ == "__main__": raise SystemExit(main()) diff --git a/skill.yaml b/skill.yaml index 9d9a3ea..aa6377e 100644 --- a/skill.yaml +++ b/skill.yaml @@ -1,5 +1,5 @@ name: paper-webpage-builder -version: 0.2.0 +version: 0.3.0 description: Build polished single-page academic project webpages from paper sources, figures, references, and validation scripts. prompts: skill: SKILL.md diff --git a/tests/golden.jsonl b/tests/golden.jsonl index 3426b55..c782881 100644 --- a/tests/golden.jsonl +++ b/tests/golden.jsonl @@ -1,3 +1,3 @@ -{"case_id":"latex-project-page","input":{"paper_source":{"path":"paper.tex","kind":"latex_project","has_figures":true,"has_tables":true},"target_output":{"directory":"web","filename":"index.html"},"goals":["project landing page","show method and results"],"constraints":["single-page html","local assets only"]},"expected":{"index_html_path":"web/index.html","modules":[{"name":"Hero"},{"name":"Method"},{"name":"Results"}],"assets":[{"path":"web/figures/overview.png","role":"figure"},{"path":"web/paper.pdf","role":"paper"}],"validation":{"link_check":"pass","html_sanity":"pass"}},"actual":{"index_html_path":"web/index.html","modules":[{"name":"Hero","purpose":"Present title, authors, venue, resource links, and primary paper claim.","content_sources":["paper title","abstract","project links"]},{"name":"Method","purpose":"Explain the core technical workflow using paper figures and concise callouts.","content_sources":["method section","figures"]},{"name":"Results","purpose":"Summarize main quantitative results and important tables.","content_sources":["results section","tables"]},{"name":"Quality Checks","purpose":"Track webpage validation status and remaining review risks.","content_sources":["link checker","browser inspection"]}],"assets":[{"path":"web/figures/overview.png","role":"figure"},{"path":"web/paper.pdf","role":"paper"}],"validation":{"link_check":"pass","html_sanity":"pass","responsive_check":"not_run"},"quality_checks":[{"name":"links","status":"pass","notes":"Local src, href, and data-figure references were checked."},{"name":"html_sanity","status":"pass","notes":"Generated HTML can be parsed by the configured sanity checker."},{"name":"responsive_layout","status":"not_run","notes":"Browser screenshots still need to be captured."},{"name":"asset_independence","status":"pass","notes":"The page does not depend on the source paper directory."}],"risks":["Browser screenshot validation still needs to be run."]},"match_mode":"partial"} -{"case_id":"pdf-with-assets-page","input":{"paper_source":{"path":"paper.pdf","kind":"pdf_with_assets","has_figures":true,"has_tables":false},"target_output":{"directory":"site","filename":"index.html"},"goals":["polished academic page","highlight dataset contribution"],"constraints":["reuse provided figures","avoid generic stock visuals"]},"expected":{"index_html_path":"site/index.html","modules":[{"name":"Hero"},{"name":"Dataset"}],"assets":[{"path":"site/figures/dataset-examples.png","role":"figure"}],"validation":{"link_check":"pass","html_sanity":"not_run"}},"actual":{"index_html_path":"site/index.html","modules":[{"name":"Hero","purpose":"Introduce the paper and primary contribution.","content_sources":["PDF metadata","abstract"]},{"name":"Dataset","purpose":"Explain dataset scope, examples, and usage value.","content_sources":["paper figures","dataset description"]},{"name":"Quality Checks","purpose":"Track extraction gaps and validation still to run.","content_sources":["link checker","manual review"]}],"assets":[{"path":"site/figures/dataset-examples.png","role":"figure"},{"path":"site/paper.pdf","role":"paper"}],"validation":{"link_check":"pass","html_sanity":"not_run","responsive_check":"not_run"},"quality_checks":[{"name":"links","status":"pass","notes":"Provided figure and PDF paths are represented in the output plan."},{"name":"html_sanity","status":"not_run","notes":"No final HTML file was generated during this deterministic runner check."},{"name":"responsive_layout","status":"not_run","notes":"Responsive browser validation requires a rendered page."}],"risks":["PDF-only extraction may miss author affiliations or table details."]},"match_mode":"partial"} -{"case_id":"existing-webpage-refresh","input":{"paper_source":{"path":"web/index.html","kind":"existing_webpage","has_figures":true,"has_tables":true},"target_output":{"directory":"web","filename":"index.html"},"goals":["refresh visual hierarchy","preserve existing resource links"],"constraints":["do not break local assets","keep page self-contained"]},"expected":{"index_html_path":"web/index.html","modules":[{"name":"Hero"},{"name":"Quality Checks"}],"assets":[{"path":"web/index.html","role":"other"}],"validation":{"link_check":"pass","responsive_check":"pass"},"quality_checks":[{"name":"links","status":"pass"},{"name":"asset_independence","status":"pass"}]},"actual":{"index_html_path":"web/index.html","modules":[{"name":"Hero","purpose":"Refresh top-level narrative while preserving primary paper links.","content_sources":["existing webpage","paper abstract"]},{"name":"Quality Checks","purpose":"Summarize validation and remaining review risks.","content_sources":["link checker","browser inspection"]}],"assets":[{"path":"web/index.html","role":"other"}],"validation":{"link_check":"pass","html_sanity":"pass","responsive_check":"pass"},"quality_checks":[{"name":"links","status":"pass","notes":"Local src, href, and data-figure references were checked."},{"name":"asset_independence","status":"pass","notes":"The page does not depend on the source paper directory."},{"name":"visual_consistency","status":"not_run","notes":"Manual design review is still required."}],"risks":["Visual consistency should be reviewed after screenshots are captured."]},"match_mode":"partial"} +{"case_id":"latex-project-page","input":{"paper_source":{"path":"paper.tex","kind":"latex_project","has_figures":true,"has_tables":true},"target_output":{"directory":"web","filename":"index.html"},"goals":["project landing page","show method and results"],"constraints":["single-page html","local assets only"]},"expected":{"index_html_path":"web/index.html","modules":[{"name":"Hero"},{"name":"Method"},{"name":"Results"}],"assets":[{"path":"web/figures/overview.png","role":"figure"},{"path":"web/paper.pdf","role":"paper"}],"validation":{"link_check":"pass","html_sanity":"pass"},"keywords":["paper webpage","method","results"]},"actual":{"index_html_path":"web/index.html","modules":[{"name":"Hero","purpose":"Present title, authors, venue, resource links, and primary paper claim.","content_sources":["paper title","abstract","project links"]},{"name":"Method","purpose":"Explain the core technical workflow using paper figures and concise callouts.","content_sources":["method section","figures"]},{"name":"Results","purpose":"Summarize main quantitative results and important tables.","content_sources":["results section","tables"]},{"name":"Quality Checks","purpose":"Track webpage validation status and remaining review risks.","content_sources":["link checker","browser inspection"]}],"assets":[{"path":"web/figures/overview.png","role":"figure"},{"path":"web/paper.pdf","role":"paper"}],"validation":{"link_check":"pass","html_sanity":"pass","responsive_check":"not_run"},"quality_checks":[{"name":"links","status":"pass","notes":"Local src, href, and data-figure references were checked."},{"name":"html_sanity","status":"pass","notes":"Generated HTML can be parsed by the configured sanity checker."},{"name":"responsive_layout","status":"not_run","notes":"Browser screenshots still need to be captured."},{"name":"asset_independence","status":"pass","notes":"The page does not depend on the source paper directory."}],"keywords":["paper webpage","method","results","local assets"],"risks":["Browser screenshot validation still needs to be run."]},"match_mode":"partial"} +{"case_id":"pdf-with-assets-page","input":{"paper_source":{"path":"paper.pdf","kind":"pdf_with_assets","has_figures":true,"has_tables":false},"target_output":{"directory":"site","filename":"index.html"},"goals":["polished academic page","highlight dataset contribution"],"constraints":["reuse provided figures","avoid generic stock visuals"]},"expected":{"index_html_path":"site/index.html","modules":[{"name":"Hero"},{"name":"Dataset"}],"assets":[{"path":"site/figures/dataset-examples.png","role":"figure"}],"validation":{"link_check":"pass","html_sanity":"not_run"},"keywords":["paper webpage","dataset"]},"actual":{"index_html_path":"site/index.html","modules":[{"name":"Hero","purpose":"Introduce the paper and primary contribution.","content_sources":["PDF metadata","abstract"]},{"name":"Dataset","purpose":"Explain dataset scope, examples, and usage value.","content_sources":["paper figures","dataset description"]},{"name":"Quality Checks","purpose":"Track extraction gaps and validation still to run.","content_sources":["link checker","manual review"]}],"assets":[{"path":"site/figures/dataset-examples.png","role":"figure"},{"path":"site/paper.pdf","role":"paper"}],"validation":{"link_check":"pass","html_sanity":"not_run","responsive_check":"not_run"},"quality_checks":[{"name":"links","status":"pass","notes":"Provided figure and PDF paths are represented in the output plan."},{"name":"html_sanity","status":"not_run","notes":"No final HTML file was generated during this deterministic runner check."},{"name":"responsive_layout","status":"not_run","notes":"Responsive browser validation requires a rendered page."}],"keywords":["paper webpage","dataset","pdf extraction","figures"],"risks":["PDF-only extraction may miss author affiliations or table details."]},"match_mode":"partial"} +{"case_id":"existing-webpage-refresh","input":{"paper_source":{"path":"web/index.html","kind":"existing_webpage","has_figures":true,"has_tables":true},"target_output":{"directory":"web","filename":"index.html"},"goals":["refresh visual hierarchy","preserve existing resource links"],"constraints":["do not break local assets","keep page self-contained"]},"expected":{"index_html_path":"web/index.html","modules":[{"name":"Hero"},{"name":"Quality Checks"}],"assets":[{"path":"web/index.html","role":"other"}],"validation":{"link_check":"pass","responsive_check":"pass"},"quality_checks":[{"name":"links","status":"pass"},{"name":"asset_independence","status":"pass"}],"keywords":["paper webpage","refresh"]},"actual":{"index_html_path":"web/index.html","modules":[{"name":"Hero","purpose":"Refresh top-level narrative while preserving primary paper links.","content_sources":["existing webpage","paper abstract"]},{"name":"Quality Checks","purpose":"Summarize validation and remaining review risks.","content_sources":["link checker","browser inspection"]}],"assets":[{"path":"web/index.html","role":"other"}],"validation":{"link_check":"pass","html_sanity":"pass","responsive_check":"pass"},"quality_checks":[{"name":"links","status":"pass","notes":"Local src, href, and data-figure references were checked."},{"name":"asset_independence","status":"pass","notes":"The page does not depend on the source paper directory."},{"name":"visual_consistency","status":"not_run","notes":"Manual design review is still required."}],"keywords":["paper webpage","refresh","quality checks","self-contained"],"risks":["Visual consistency should be reviewed after screenshots are captured."]},"match_mode":"partial"} From a4d0f92186c3e4df86e2bdc91b395cd0adbbb4b5 Mon Sep 17 00:00:00 2001 From: xxllovemkm Date: Sat, 16 May 2026 00:33:00 +0800 Subject: [PATCH 4/6] quality: strengthen paper webpage skill guidance --- SKILL.md | 15 +- references/design_principles.md | 30 +- references/module_patterns.md | 19 +- .../closure-notes.md | 40 ++ reports/skill-quality-loop-20260516/diff.json | 73 ++++ reports/skill-quality-loop-20260516/diff.txt | 6 + .../git-diff-files.txt | 18 + .../git-diff-stat.txt | 19 + .../skill-quality-loop-20260516/pr-summary.md | 25 ++ .../sit-report.html | 398 ++++++++++++++++++ .../sit-report.json | 119 ++++++ .../skill-quality-loop-20260516/sit-report.md | 56 +++ .../sit-test-run.json | 49 +++ .../sit-test-run.txt | 7 + .../skill-quality-loop-20260516/sit-test.txt | 4 + .../skill-quality-loop-20260516/validate.txt | 13 + scripts/scan_paper.py | 42 +- skill.yaml | 2 +- 18 files changed, 923 insertions(+), 12 deletions(-) create mode 100644 reports/skill-quality-loop-20260516/closure-notes.md create mode 100644 reports/skill-quality-loop-20260516/diff.json create mode 100644 reports/skill-quality-loop-20260516/diff.txt create mode 100644 reports/skill-quality-loop-20260516/git-diff-files.txt create mode 100644 reports/skill-quality-loop-20260516/git-diff-stat.txt create mode 100644 reports/skill-quality-loop-20260516/pr-summary.md create mode 100644 reports/skill-quality-loop-20260516/sit-report.html create mode 100644 reports/skill-quality-loop-20260516/sit-report.json create mode 100644 reports/skill-quality-loop-20260516/sit-report.md create mode 100644 reports/skill-quality-loop-20260516/sit-test-run.json create mode 100644 reports/skill-quality-loop-20260516/sit-test-run.txt create mode 100644 reports/skill-quality-loop-20260516/sit-test.txt create mode 100644 reports/skill-quality-loop-20260516/validate.txt diff --git a/SKILL.md b/SKILL.md index 1cb24ce..45d7d4d 100644 --- a/SKILL.md +++ b/SKILL.md @@ -11,15 +11,22 @@ Use this skill to turn a paper project folder into a web-ready project page. It Do not mechanically clone an existing webpage. Use prior pages only as references for interaction patterns and content completeness. The final design must follow the target paper's topic, figures, color palette, density, and audience. +Two failure modes to actively avoid: + +- Do not reuse a background system from another paper page by default. Grids, coordinate paper, dark sections, gradients, or canvas textures are allowed only when they are supported by the target paper's own figures, domain, or visual language. +- Do not summarize away central evidence. If the paper's main claim depends on a main experiment table, benchmark comparison table, dataset statistics table, or ablation table, the webpage must include that table in full or provide a clearly equivalent full presentation. + ## Workflow 1. Inspect inputs before editing. - Locate paper source, PDF, figures, logos, existing `template.html`, and target `index.html`. - Run `scripts/scan_paper.py ` when a TeX source exists. - - Identify important tables as well as figures; main results, dataset statistics, and ablations usually belong on the page. + - Build a table ledger before designing: caption, label, section, whether it is main evidence, and whether it must appear fully on the page. + - Identify important tables as well as figures; main results, benchmark comparisons, dataset statistics, and ablations usually belong on the page. 2. Build a content map. - Extract title, authors, affiliations, abstract claim, contributions, links, dataset stats, method description, main results, case studies, citation. + - Map every central table to a page module. If a central table is too large, plan a scrollable/grouped table rather than dropping rows. - Decide modules before writing. Typical modules: Hero, Motivation, Method, Dataset/Benchmark, Results, Case Study, Citation. - Read `references/module_patterns.md` when choosing sections or table placement. @@ -30,18 +37,22 @@ Do not mechanically clone an existing webpage. Use prior pages only as reference - Rename web assets to stable ASCII filenames. 4. Design the page around the paper. - - Derive colors from the paper's key figures and domain. For geometry/GUI papers, grids, coordinate-paper textures, and precise callouts are often appropriate. + - Derive colors, background, spacing, figure framing, and motifs from the paper's key figures and domain. + - First list the paper-specific visual cues, then choose the background. A plain surface, soft paper tone, lab-notebook grid, dark canvas, figure-derived gradient, or no visible texture are all valid; none is the default. - Keep background transitions coherent across sections. Avoid abrupt dark-to-light jumps unless the entire page system intentionally supports that contrast. - Read `references/design_principles.md` when deciding visual style or revising design feedback. 5. Implement the webpage. - Produce a self-contained single-page `index.html` unless the repo already has a framework. - Include responsive navigation, resource buttons, figure zoom/modal behavior, and readable tables. + - Include full central tables with horizontal scroll on mobile, sticky headers when useful, grouped rows when needed, and highlights for the proposed method or best values. - Use charts only when they clarify a key result beyond the paper figures. - Avoid hidden dependency on the source paper directory; generated page should work from the webpage folder. 6. Validate. - Run `scripts/check_webpage_links.py ` to verify local `src`/`href` assets. + - Reconcile the table ledger against the page: every central table is included fully or has an explicit reason and equivalent representation. + - Recheck the design for inherited style artifacts: background, palette, hero layout, and cards should match this paper rather than a prior generated page. - Run available HTML sanity checks, such as `xmllint --html --noout`. - If browser tooling exists, capture desktop/mobile screenshots and check for overflow, missing assets, or poor contrast. - Report any validation you could not run. diff --git a/references/design_principles.md b/references/design_principles.md index fe12a22..dad4641 100644 --- a/references/design_principles.md +++ b/references/design_principles.md @@ -4,11 +4,20 @@ Derive the visual system from the paper's topic and figures: -- Geometry/GUI/control: coordinate grids, canvas surfaces, blue/orange callouts, precise strokes. +- Geometry/GUI/control: coordinate grids, canvas surfaces, blue/orange callouts, precise strokes when those cues are present in the paper. - Benchmark/data papers: dense tables, taxonomies, clear statistics, restrained color. - Agent/tool papers: workflow diagrams, tool icons, process timelines. - Scientific visualization: larger figures, neutral backgrounds, minimal decoration. +Before writing CSS, make a short visual-cue inventory from the target paper: + +- dominant figure colors and contrast level; +- recurring geometry, interface, or scientific motifs; +- figure density and whether users need to inspect fine details; +- tone: benchmark report, systems demo, scientific visualization, productized dataset, or method paper. + +Use that inventory to justify the page substrate. If the inventory does not support a grid, do not use one. + ## Avoid Template Cloning Use reference pages for interaction mechanics, not surface style. Change at least: @@ -20,15 +29,29 @@ Use reference pages for interaction mechanics, not surface style. Change at leas - Table treatment. - Hero layout. +Treat the following as clone indicators that require revision: + +- the same grid/background texture used for unrelated papers; +- the same card-heavy rhythm when the target paper is table- or figure-led; +- color accents not found in, or naturally derived from, the paper figures; +- a hero that copies another page's composition instead of foregrounding this paper's strongest figure or result. + ## Background Coherence Keep section transitions natural: - Prefer one shared page substrate with subtle section tinting. - Avoid abrupt full dark sections beside bright sections unless repeated consistently. -- Use thin separators, soft gradients, or grid density changes for transitions. +- Use thin separators, soft gradients, figure-derived washes, plain neutral surfaces, or grid density changes only when they match the paper. - Hero and footer should feel related to the rest of the page. +Background options are choices, not defaults: + +- Plain neutral surface for dense benchmark/result pages. +- Figure-derived color wash for visual generation or multimodal papers with strong image palettes. +- Coordinate/grid substrate only for papers where construction, coordinates, GUI canvases, or figure motifs make it semantically useful. +- Dark canvas only when the paper's media or application benefits from inspection against a dark surface. + ## Layout - Put the actual project content above the fold: title, claim, links, headline metrics, and a key figure. @@ -41,7 +64,8 @@ Keep section transitions natural: - All local images, PDFs, and linked assets exist. - Figures are web formats and have stable filenames. -- Main result and dataset statistic tables are present when central. +- Main result and dataset statistic tables are present in full when central. +- The background and palette can be traced to the target paper, not to a prior generated page. - Mobile navigation works. - Text does not overlap figures/cards/buttons. - Page still works without access to the original paper source directory. diff --git a/references/module_patterns.md b/references/module_patterns.md index ab37e3c..afbcee1 100644 --- a/references/module_patterns.md +++ b/references/module_patterns.md @@ -8,7 +8,7 @@ Use the paper's own narrative, not a fixed template. These patterns are defaults - Motivation: what gap the paper identifies, why existing work fails, one strong overview/motivation figure. - Method: 2-4 conceptual steps or modules, plus the main framework figure. - Dataset/Benchmark: data construction pipeline, split/statistics, taxonomy/category figure, key table. -- Results: main result table in full when it is central; compact chart or takeaway cards; appendix figures if useful. +- Results: main result table in full when it is central; benchmark comparison table when it supports the paper's novelty claim; compact chart or takeaway cards; appendix figures if useful. - Case Study: one qualitative figure, error analysis, human study or alignment plot. - Citation: BibTeX draft with a warning if arXiv/venue data is not final. @@ -16,10 +16,21 @@ Use the paper's own narrative, not a fixed template. These patterns are defaults Tables should enter the webpage when they carry the core claim. -- Main results: include the full table, not just a summary. -- Dataset stats: include a compact web table or metric grid. -- Ablations: include when the user asks for completeness or the paper's method depends on them. +- Start with a table ledger: caption, label, source section, row/column scope, centrality, and page destination. +- Main results: include the full table, not just a summary or selected rows. +- Benchmark comparisons: include in full when the paper uses them to claim coverage, novelty, or state-of-the-art positioning. +- Dataset stats: include a compact web table or metric grid, but preserve all dimensions that support the claim. +- Ablations: include when the user asks for completeness, the paper's method depends on them, or the abstract/conclusion cites them. - Large tables: wrap in horizontal scroll, keep sticky headers if practical, group rows by model family, highlight the proposed method and best values. +- If a table is too wide, split it by metric group or model family while preserving all rows and values. +- If a figure duplicates a table, the table can be represented visually only when all values remain inspectable or a linked full table is present. + +Before finishing, reconcile the ledger: + +- each central table is present fully; +- each non-central omitted table has a reason; +- displayed values match the source table; +- captions make clear when a table is selected, abbreviated, or full. ## Figure Selection diff --git a/reports/skill-quality-loop-20260516/closure-notes.md b/reports/skill-quality-loop-20260516/closure-notes.md new file mode 100644 index 0000000..66cd58f --- /dev/null +++ b/reports/skill-quality-loop-20260516/closure-notes.md @@ -0,0 +1,40 @@ +# Skill Quality Loop Closure Notes + +Date: 2026-05-16 + +## Goal + +Validate the updated `paper-webpage-builder` skill after the GGBench trial feedback: + +- avoid reused/default grid backgrounds; +- require paper-specific visual cue selection; +- require full central experiment tables; +- improve LaTeX table discovery in `scan_paper.py`. + +## SitHub Loop Result + +- `sit validate`: pass +- `sit test`: pass, 3/3 golden cases +- `sit test --run`: pass, 3/3 runner-backed golden cases +- `sit diff /tmp/paper-webpage-builder-test .`: `review-required` +- suggested version bump: `minor` +- package version bumped: `0.3.0` -> `0.4.0` + +## Important Finding + +SitHub semantic diff detected the prompt/reference changes: + +- `skill.yaml` version bump +- `SKILL.md` +- `references/design_principles.md` +- `references/module_patterns.md` + +It did not detect the changed helper script: + +- `scripts/scan_paper.py` + +The Git diff artifact captures the script change, but the SitHub semantic diff model currently does not include tool/script resources. This is a real product gap for Skill packages because script behavior can change generated outputs. + +## Reusable Precheck + +Before closing any SitHub loop, compare `sit diff` against `git diff --name-only`. If Git reports changed files that are absent from the semantic diff, record them as an observability gap or extend the Skill manifest/diff model to cover them. diff --git a/reports/skill-quality-loop-20260516/diff.json b/reports/skill-quality-loop-20260516/diff.json new file mode 100644 index 0000000..1eb358c --- /dev/null +++ b/reports/skill-quality-loop-20260516/diff.json @@ -0,0 +1,73 @@ +{ + "schema_version": "sit.diff.v1", + "old": { + "name": "paper-webpage-builder", + "version": "0.3.0", + "root": "/tmp/sit-ref-eybqh_jk/old", + "manifest": "/tmp/sit-ref-eybqh_jk/old/skill.yaml", + "source": "HEAD~1" + }, + "new": { + "name": "paper-webpage-builder", + "version": "0.4.0", + "root": "/tmp/sit-ref-eybqh_jk/new", + "manifest": "/tmp/sit-ref-eybqh_jk/new/skill.yaml", + "source": "HEAD" + }, + "changed": true, + "breaking": false, + "risk": "review-required", + "suggested_bump": "minor", + "messages": [ + "PACKAGE paper-webpage-builder@0.3.0 -> paper-webpage-builder@0.4.0", + "MANIFEST changed version: '0.3.0' -> '0.4.0'", + "PROMPT changed design_principles: design_principles.md -> design_principles.md", + "PROMPT changed module_patterns: module_patterns.md -> module_patterns.md", + "PROMPT changed skill: SKILL.md -> SKILL.md", + "RISK review-required" + ], + "events": [ + { + "category": "package", + "severity": "info", + "changed": false, + "breaking": false, + "message": "PACKAGE paper-webpage-builder@0.3.0 -> paper-webpage-builder@0.4.0" + }, + { + "category": "manifest", + "severity": "changed", + "changed": true, + "breaking": false, + "message": "MANIFEST changed version: '0.3.0' -> '0.4.0'" + }, + { + "category": "prompt", + "severity": "changed", + "changed": true, + "breaking": false, + "message": "PROMPT changed design_principles: design_principles.md -> design_principles.md" + }, + { + "category": "prompt", + "severity": "changed", + "changed": true, + "breaking": false, + "message": "PROMPT changed module_patterns: module_patterns.md -> module_patterns.md" + }, + { + "category": "prompt", + "severity": "changed", + "changed": true, + "breaking": false, + "message": "PROMPT changed skill: SKILL.md -> SKILL.md" + }, + { + "category": "risk", + "severity": "changed", + "changed": true, + "breaking": false, + "message": "RISK review-required" + } + ] +} diff --git a/reports/skill-quality-loop-20260516/diff.txt b/reports/skill-quality-loop-20260516/diff.txt new file mode 100644 index 0000000..83ffa00 --- /dev/null +++ b/reports/skill-quality-loop-20260516/diff.txt @@ -0,0 +1,6 @@ +PACKAGE paper-webpage-builder@0.3.0 -> paper-webpage-builder@0.4.0 +MANIFEST changed version: '0.3.0' -> '0.4.0' +PROMPT changed design_principles: design_principles.md -> design_principles.md +PROMPT changed module_patterns: module_patterns.md -> module_patterns.md +PROMPT changed skill: SKILL.md -> SKILL.md +RISK review-required diff --git a/reports/skill-quality-loop-20260516/git-diff-files.txt b/reports/skill-quality-loop-20260516/git-diff-files.txt new file mode 100644 index 0000000..be09cde --- /dev/null +++ b/reports/skill-quality-loop-20260516/git-diff-files.txt @@ -0,0 +1,18 @@ +SKILL.md +references/design_principles.md +references/module_patterns.md +reports/skill-quality-loop-20260516/closure-notes.md +reports/skill-quality-loop-20260516/diff.json +reports/skill-quality-loop-20260516/diff.txt +reports/skill-quality-loop-20260516/git-diff-files.txt +reports/skill-quality-loop-20260516/git-diff-stat.txt +reports/skill-quality-loop-20260516/pr-summary.md +reports/skill-quality-loop-20260516/sit-report.html +reports/skill-quality-loop-20260516/sit-report.json +reports/skill-quality-loop-20260516/sit-report.md +reports/skill-quality-loop-20260516/sit-test-run.json +reports/skill-quality-loop-20260516/sit-test-run.txt +reports/skill-quality-loop-20260516/sit-test.txt +reports/skill-quality-loop-20260516/validate.txt +scripts/scan_paper.py +skill.yaml diff --git a/reports/skill-quality-loop-20260516/git-diff-stat.txt b/reports/skill-quality-loop-20260516/git-diff-stat.txt new file mode 100644 index 0000000..73c8e05 --- /dev/null +++ b/reports/skill-quality-loop-20260516/git-diff-stat.txt @@ -0,0 +1,19 @@ + SKILL.md | 15 +- + references/design_principles.md | 30 +- + references/module_patterns.md | 19 +- + .../skill-quality-loop-20260516/closure-notes.md | 40 +++ + reports/skill-quality-loop-20260516/diff.json | 73 ++++ + reports/skill-quality-loop-20260516/diff.txt | 6 + + .../skill-quality-loop-20260516/git-diff-files.txt | 5 + + .../skill-quality-loop-20260516/git-diff-stat.txt | 6 + + reports/skill-quality-loop-20260516/pr-summary.md | 25 ++ + .../skill-quality-loop-20260516/sit-report.html | 398 +++++++++++++++++++++ + .../skill-quality-loop-20260516/sit-report.json | 119 ++++++ + reports/skill-quality-loop-20260516/sit-report.md | 56 +++ + .../skill-quality-loop-20260516/sit-test-run.json | 49 +++ + .../skill-quality-loop-20260516/sit-test-run.txt | 7 + + reports/skill-quality-loop-20260516/sit-test.txt | 4 + + reports/skill-quality-loop-20260516/validate.txt | 13 + + scripts/scan_paper.py | 42 ++- + skill.yaml | 2 +- + 18 files changed, 897 insertions(+), 12 deletions(-) diff --git a/reports/skill-quality-loop-20260516/pr-summary.md b/reports/skill-quality-loop-20260516/pr-summary.md new file mode 100644 index 0000000..657130b --- /dev/null +++ b/reports/skill-quality-loop-20260516/pr-summary.md @@ -0,0 +1,25 @@ +## Skill Change Summary + +- Baseline: `paper-webpage-builder@0.3.0` +- Current: `paper-webpage-builder@0.4.0` +- Validation: pass +- Golden tests: pass +- Risk: `review-required` +- Suggested version bump: `minor` + +### Semantic Diff + +- `PACKAGE paper-webpage-builder@0.3.0 -> paper-webpage-builder@0.4.0` +- `MANIFEST changed version: '0.3.0' -> '0.4.0'` +- `PROMPT changed design_principles: design_principles.md -> design_principles.md` +- `PROMPT changed module_patterns: module_patterns.md -> module_patterns.md` +- `PROMPT changed skill: SKILL.md -> SKILL.md` +- `RISK review-required` + +### Reproduce + +```bash +sit validate . +sit test . +sit diff HEAD~1..HEAD +``` diff --git a/reports/skill-quality-loop-20260516/sit-report.html b/reports/skill-quality-loop-20260516/sit-report.html new file mode 100644 index 0000000..61af010 --- /dev/null +++ b/reports/skill-quality-loop-20260516/sit-report.html @@ -0,0 +1,398 @@ + + + + + +paper-webpage-builder 0.4.0 SitHub Report + + + +
+
+
+

SitHub semantic control report

+

paper-webpage-builder

+

Version 0.4.0 - 2026-05-16

+
+
+Diff risk +review-required +Suggested bump: minor +
+
+
+
+Validation +pass +
+
+Golden Tests +pass +
+
+Cases Passed +3/3 +
+
+Version Bump +minor +
+
+
+
+

Golden Test Progress

+3/3 +
+
+
+
+
+
+
+

Validation

+
    +
  • OK name: paper-webpage-builder
  • +
  • OK version: 0.4.0
  • +
  • OK manifest exists: /tmp/sit-ref-j0t0811l/new/skill.yaml
  • +
  • OK prompt.skill exists: /tmp/sit-ref-j0t0811l/new/SKILL.md
  • +
  • OK prompt.module_patterns exists: /tmp/sit-ref-j0t0811l/new/references/module_patterns.md
  • +
  • OK prompt.design_principles exists: /tmp/sit-ref-j0t0811l/new/references/design_principles.md
  • +
  • OK schema.input exists: /tmp/sit-ref-j0t0811l/new/schemas/input.schema.json
  • +
  • OK schema.output exists: /tmp/sit-ref-j0t0811l/new/schemas/output.schema.json
  • +
  • OK test.golden exists: /tmp/sit-ref-j0t0811l/new/tests/golden.jsonl
  • +
  • OK schema.input JSON schema valid
  • +
  • OK schema.output JSON schema valid
  • +
  • OK test.golden JSONL parsed: 3 cases
  • +
  • OK command.run_case: configured
  • +
+
+
+

Golden Tests

+
    +
  • OK latex-project-page: partial match passed
  • +
  • OK pdf-with-assets-page: partial match passed
  • +
  • OK existing-webpage-refresh: partial match passed
  • +
  • SUMMARY 3/3 golden cases passed
  • +
+
+
+
+
+

Semantic Diff

+review-required +
+
+ + + + + +
+
+
+
+package +#1 +
+ +

PACKAGE paper-webpage-builder@0.3.0 -> paper-webpage-builder@0.4.0

+
+
+
+manifest +#2 +
+ +

MANIFEST changed version: '0.3.0' -> '0.4.0'

+
+
+
+prompt +#3 +
+ +

PROMPT changed design_principles: design_principles.md -> design_principles.md

+
+
+
+prompt +#4 +
+ +

PROMPT changed module_patterns: module_patterns.md -> module_patterns.md

+
+
+
+prompt +#5 +
+ +

PROMPT changed skill: SKILL.md -> SKILL.md

+
+
+
+risk +#6 +
+ +

RISK review-required

+
+
+
+
+
+

Reproduce

+local commands +
+

+python3 -m sit.cli validate .
+python3 -m sit.cli test .
+python3 -m sit.cli diff HEAD~1..HEAD
+
+
+ +
+ + diff --git a/reports/skill-quality-loop-20260516/sit-report.json b/reports/skill-quality-loop-20260516/sit-report.json new file mode 100644 index 0000000..52a8311 --- /dev/null +++ b/reports/skill-quality-loop-20260516/sit-report.json @@ -0,0 +1,119 @@ +{ + "schema_version": "sit.report.v1", + "date": "2026-05-16", + "package": { + "name": "paper-webpage-builder", + "version": "0.4.0", + "description": "Build polished single-page academic project webpages from paper sources, figures, references, and validation scripts.", + "root": "/tmp/sit-ref-qbmhnyph/new", + "manifest": "/tmp/sit-ref-qbmhnyph/new/skill.yaml" + }, + "validation": { + "status": "pass", + "ok": true, + "messages": [ + "OK name: paper-webpage-builder", + "OK version: 0.4.0", + "OK manifest exists: /tmp/sit-ref-qbmhnyph/new/skill.yaml", + "OK prompt.skill exists: /tmp/sit-ref-qbmhnyph/new/SKILL.md", + "OK prompt.module_patterns exists: /tmp/sit-ref-qbmhnyph/new/references/module_patterns.md", + "OK prompt.design_principles exists: /tmp/sit-ref-qbmhnyph/new/references/design_principles.md", + "OK schema.input exists: /tmp/sit-ref-qbmhnyph/new/schemas/input.schema.json", + "OK schema.output exists: /tmp/sit-ref-qbmhnyph/new/schemas/output.schema.json", + "OK test.golden exists: /tmp/sit-ref-qbmhnyph/new/tests/golden.jsonl", + "OK schema.input JSON schema valid", + "OK schema.output JSON schema valid", + "OK test.golden JSONL parsed: 3 cases", + "OK command.run_case: configured" + ] + }, + "golden_tests": { + "status": "pass", + "ok": true, + "passed": 3, + "total": 3, + "summary": "SUMMARY 3/3 golden cases passed", + "messages": [ + "OK latex-project-page: partial match passed", + "OK pdf-with-assets-page: partial match passed", + "OK existing-webpage-refresh: partial match passed", + "SUMMARY 3/3 golden cases passed" + ] + }, + "diff": { + "schema_version": "sit.diff.v1", + "old": { + "name": "paper-webpage-builder", + "version": "0.3.0", + "root": "/tmp/sit-ref-qbmhnyph/old", + "manifest": "/tmp/sit-ref-qbmhnyph/old/skill.yaml" + }, + "new": { + "name": "paper-webpage-builder", + "version": "0.4.0", + "root": "/tmp/sit-ref-qbmhnyph/new", + "manifest": "/tmp/sit-ref-qbmhnyph/new/skill.yaml" + }, + "changed": true, + "breaking": false, + "risk": "review-required", + "suggested_bump": "minor", + "messages": [ + "PACKAGE paper-webpage-builder@0.3.0 -> paper-webpage-builder@0.4.0", + "MANIFEST changed version: '0.3.0' -> '0.4.0'", + "PROMPT changed design_principles: design_principles.md -> design_principles.md", + "PROMPT changed module_patterns: module_patterns.md -> module_patterns.md", + "PROMPT changed skill: SKILL.md -> SKILL.md", + "RISK review-required" + ], + "events": [ + { + "category": "package", + "severity": "info", + "changed": false, + "breaking": false, + "message": "PACKAGE paper-webpage-builder@0.3.0 -> paper-webpage-builder@0.4.0" + }, + { + "category": "manifest", + "severity": "changed", + "changed": true, + "breaking": false, + "message": "MANIFEST changed version: '0.3.0' -> '0.4.0'" + }, + { + "category": "prompt", + "severity": "changed", + "changed": true, + "breaking": false, + "message": "PROMPT changed design_principles: design_principles.md -> design_principles.md" + }, + { + "category": "prompt", + "severity": "changed", + "changed": true, + "breaking": false, + "message": "PROMPT changed module_patterns: module_patterns.md -> module_patterns.md" + }, + { + "category": "prompt", + "severity": "changed", + "changed": true, + "breaking": false, + "message": "PROMPT changed skill: SKILL.md -> SKILL.md" + }, + { + "category": "risk", + "severity": "changed", + "changed": true, + "breaking": false, + "message": "RISK review-required" + } + ] + }, + "reproducibility": { + "validate": "python3 -m sit.cli validate .", + "test": "python3 -m sit.cli test .", + "diff": "python3 -m sit.cli diff HEAD~1..HEAD" + } +} diff --git a/reports/skill-quality-loop-20260516/sit-report.md b/reports/skill-quality-loop-20260516/sit-report.md new file mode 100644 index 0000000..9bd1962 --- /dev/null +++ b/reports/skill-quality-loop-20260516/sit-report.md @@ -0,0 +1,56 @@ +# paper-webpage-builder 0.4.0 SIT Report + +Date: 2026-05-16 + +## Package + +- Name: `paper-webpage-builder` +- Version: `0.4.0` +- Root: `/tmp/sit-ref-5k01_r1z/new` +- Manifest: `/tmp/sit-ref-5k01_r1z/new/skill.yaml` +- Description: Build polished single-page academic project webpages from paper sources, figures, references, and validation scripts. + +## Validation + +- Result: pass +- `OK name: paper-webpage-builder` +- `OK version: 0.4.0` +- `OK manifest exists: /tmp/sit-ref-5k01_r1z/new/skill.yaml` +- `OK prompt.skill exists: /tmp/sit-ref-5k01_r1z/new/SKILL.md` +- `OK prompt.module_patterns exists: /tmp/sit-ref-5k01_r1z/new/references/module_patterns.md` +- `OK prompt.design_principles exists: /tmp/sit-ref-5k01_r1z/new/references/design_principles.md` +- `OK schema.input exists: /tmp/sit-ref-5k01_r1z/new/schemas/input.schema.json` +- `OK schema.output exists: /tmp/sit-ref-5k01_r1z/new/schemas/output.schema.json` +- `OK test.golden exists: /tmp/sit-ref-5k01_r1z/new/tests/golden.jsonl` +- `OK schema.input JSON schema valid` +- `OK schema.output JSON schema valid` +- `OK test.golden JSONL parsed: 3 cases` +- `OK command.run_case: configured` + +## Golden Tests + +- Result: pass +- `OK latex-project-page: partial match passed` +- `OK pdf-with-assets-page: partial match passed` +- `OK existing-webpage-refresh: partial match passed` +- `SUMMARY 3/3 golden cases passed` + +## Diff + +- Baseline: `paper-webpage-builder@0.3.0` +- Current: `paper-webpage-builder@0.4.0` +- `PACKAGE paper-webpage-builder@0.3.0 -> paper-webpage-builder@0.4.0` +- `MANIFEST changed version: '0.3.0' -> '0.4.0'` +- `PROMPT changed design_principles: design_principles.md -> design_principles.md` +- `PROMPT changed module_patterns: module_patterns.md -> module_patterns.md` +- `PROMPT changed skill: SKILL.md -> SKILL.md` +- `RISK review-required` + +## Reproducibility + +- Re-run validation with: + `python3 -m sit.cli validate .` +- Re-run golden schema tests with: + `python3 -m sit.cli test .` +- Re-run package diff with: + `python3 -m sit.cli diff HEAD~1..HEAD` diff --git a/reports/skill-quality-loop-20260516/sit-test-run.json b/reports/skill-quality-loop-20260516/sit-test-run.json new file mode 100644 index 0000000..841df35 --- /dev/null +++ b/reports/skill-quality-loop-20260516/sit-test-run.json @@ -0,0 +1,49 @@ +{ + "schema_version": "sit.test.v1", + "package": { + "name": "paper-webpage-builder", + "version": "0.4.0", + "root": "/mnt/shared-storage-user/xuxinglong-p/paper-webpage-builder", + "manifest": "/mnt/shared-storage-user/xuxinglong-p/paper-webpage-builder/skill.yaml" + }, + "execution": { + "mode": "runner", + "runner": "python3 scripts/sit_run_case.py --input {input} --output {output}", + "timeout_seconds": 30 + }, + "validation": { + "status": "pass", + "ok": true, + "messages": [ + "OK name: paper-webpage-builder", + "OK version: 0.4.0", + "OK manifest exists: /mnt/shared-storage-user/xuxinglong-p/paper-webpage-builder/skill.yaml", + "OK prompt.skill exists: /mnt/shared-storage-user/xuxinglong-p/paper-webpage-builder/SKILL.md", + "OK prompt.module_patterns exists: /mnt/shared-storage-user/xuxinglong-p/paper-webpage-builder/references/module_patterns.md", + "OK prompt.design_principles exists: /mnt/shared-storage-user/xuxinglong-p/paper-webpage-builder/references/design_principles.md", + "OK schema.input exists: /mnt/shared-storage-user/xuxinglong-p/paper-webpage-builder/schemas/input.schema.json", + "OK schema.output exists: /mnt/shared-storage-user/xuxinglong-p/paper-webpage-builder/schemas/output.schema.json", + "OK test.golden exists: /mnt/shared-storage-user/xuxinglong-p/paper-webpage-builder/tests/golden.jsonl", + "OK schema.input JSON schema valid", + "OK schema.output JSON schema valid", + "OK test.golden JSONL parsed: 3 cases", + "OK command.run_case: configured" + ] + }, + "golden_tests": { + "status": "pass", + "ok": true, + "passed": 3, + "total": 3, + "summary": "SUMMARY 3/3 golden cases passed", + "messages": [ + "OK latex-project-page: runner produced actual", + "OK latex-project-page: partial match passed", + "OK pdf-with-assets-page: runner produced actual", + "OK pdf-with-assets-page: partial match passed", + "OK existing-webpage-refresh: runner produced actual", + "OK existing-webpage-refresh: partial match passed", + "SUMMARY 3/3 golden cases passed" + ] + } +} diff --git a/reports/skill-quality-loop-20260516/sit-test-run.txt b/reports/skill-quality-loop-20260516/sit-test-run.txt new file mode 100644 index 0000000..37dd4a0 --- /dev/null +++ b/reports/skill-quality-loop-20260516/sit-test-run.txt @@ -0,0 +1,7 @@ +OK latex-project-page: runner produced actual +OK latex-project-page: partial match passed +OK pdf-with-assets-page: runner produced actual +OK pdf-with-assets-page: partial match passed +OK existing-webpage-refresh: runner produced actual +OK existing-webpage-refresh: partial match passed +SUMMARY 3/3 golden cases passed diff --git a/reports/skill-quality-loop-20260516/sit-test.txt b/reports/skill-quality-loop-20260516/sit-test.txt new file mode 100644 index 0000000..134daa4 --- /dev/null +++ b/reports/skill-quality-loop-20260516/sit-test.txt @@ -0,0 +1,4 @@ +OK latex-project-page: partial match passed +OK pdf-with-assets-page: partial match passed +OK existing-webpage-refresh: partial match passed +SUMMARY 3/3 golden cases passed diff --git a/reports/skill-quality-loop-20260516/validate.txt b/reports/skill-quality-loop-20260516/validate.txt new file mode 100644 index 0000000..0d95db7 --- /dev/null +++ b/reports/skill-quality-loop-20260516/validate.txt @@ -0,0 +1,13 @@ +OK name: paper-webpage-builder +OK version: 0.4.0 +OK manifest exists: /mnt/shared-storage-user/xuxinglong-p/paper-webpage-builder/skill.yaml +OK prompt.skill exists: /mnt/shared-storage-user/xuxinglong-p/paper-webpage-builder/SKILL.md +OK prompt.module_patterns exists: /mnt/shared-storage-user/xuxinglong-p/paper-webpage-builder/references/module_patterns.md +OK prompt.design_principles exists: /mnt/shared-storage-user/xuxinglong-p/paper-webpage-builder/references/design_principles.md +OK schema.input exists: /mnt/shared-storage-user/xuxinglong-p/paper-webpage-builder/schemas/input.schema.json +OK schema.output exists: /mnt/shared-storage-user/xuxinglong-p/paper-webpage-builder/schemas/output.schema.json +OK test.golden exists: /mnt/shared-storage-user/xuxinglong-p/paper-webpage-builder/tests/golden.jsonl +OK schema.input JSON schema valid +OK schema.output JSON schema valid +OK test.golden JSONL parsed: 3 cases +OK command.run_case: configured diff --git a/scripts/scan_paper.py b/scripts/scan_paper.py index fbee4ec..b68564a 100755 --- a/scripts/scan_paper.py +++ b/scripts/scan_paper.py @@ -47,6 +47,20 @@ def clean_latex(value: str) -> str: return value.strip() +def approx_table_rows(table_body: str) -> int: + """Return a rough count of data rows in a LaTeX table body.""" + tabular = re.search( + r"\\begin\{(?:tabular|tabularx|array)\}(?:\{[^{}]*\}){1,2}(.*?)\\end\{(?:tabular|tabularx|array)\}", + table_body, + re.S, + ) + if not tabular: + return 0 + body = re.sub(r"\\(?:toprule|midrule|bottomrule|hline|cline\{[^{}]*\})", "", tabular.group(1)) + rows = [row for row in re.split(r"\\\\", body) if "&" in row and clean_latex(row)] + return len(rows) + + def main() -> int: parser = argparse.ArgumentParser() parser.add_argument("paper_tex", type=Path) @@ -93,8 +107,32 @@ def main() -> int: print() print("Tables:") - for caption in re.finditer(r"\\begin\{table\*?\}.*?\\caption\{(.*?)\}.*?\\end\{table\*?\}", text, re.S): - print(f"- {clean_latex(caption.group(1))[:260]}") + table_pattern = re.compile(r"\\begin\{(table\*?)\}(?:\[[^\]]*\])?(.*?)\\end\{\1\}", re.S) + found_tables = False + for index, table in enumerate(table_pattern.finditer(text), 1): + found_tables = True + body = table.group(2) + captions = find_braced("caption", body) + labels = find_braced("label", body) + caption = clean_latex(captions[0])[:260] if captions else "(no caption)" + label = clean_latex(labels[0]) if labels else "(no label)" + line = text[: table.start()].count("\n") + 1 + rows = approx_table_rows(body) + row_note = f", approx rows: {rows}" if rows else "" + print(f"- Table {index} at line {line}, label: {label}{row_note}: {caption}") + + longtable_pattern = re.compile(r"\\begin\{longtable\}(?:\{[^{}]*\})?(.*?)\\end\{longtable\}", re.S) + for index, table in enumerate(longtable_pattern.finditer(text), 1): + found_tables = True + body = table.group(1) + captions = find_braced("caption", body) + labels = find_braced("label", body) + caption = clean_latex(captions[0])[:260] if captions else "(no caption)" + label = clean_latex(labels[0]) if labels else "(no label)" + line = text[: table.start()].count("\n") + 1 + print(f"- Longtable {index} at line {line}, label: {label}: {caption}") + if not found_tables: + print("- (none detected)") print() links = re.findall(r"\\href\{([^{}]+)\}\{([^{}]+)\}|\\url\{([^{}]+)\}", text) diff --git a/skill.yaml b/skill.yaml index aa6377e..51adccb 100644 --- a/skill.yaml +++ b/skill.yaml @@ -1,5 +1,5 @@ name: paper-webpage-builder -version: 0.3.0 +version: 0.4.0 description: Build polished single-page academic project webpages from paper sources, figures, references, and validation scripts. prompts: skill: SKILL.md From 5c115428e14c3e14837d2764aed17e9a1ffa46eb Mon Sep 17 00:00:00 2001 From: xxllovemkm Date: Mon, 18 May 2026 11:21:31 +0800 Subject: [PATCH 5/6] feat: improve paper asset extraction workflow --- SKILL.md | 25 +- reports/optimization-rerun-20260517/diff.json | 125 +++++ reports/optimization-rerun-20260517/diff.txt | 90 ++++ .../optimization-rerun-20260517/pr-summary.md | 33 ++ .../sit-report.html | 430 ++++++++++++++++++ .../sit-report.json | 171 +++++++ .../optimization-rerun-20260517/sit-report.md | 60 +++ .../sit-test-run.txt | 10 + .../optimization-rerun-20260517/validate.txt | 13 + scripts/convert_figures.py | 335 ++++++++++++++ scripts/convert_figures.sh | 22 +- scripts/extract_citation.py | 267 +++++++++++ scripts/extract_tables.py | 387 ++++++++++++++++ scripts/scan_paper.py | 57 ++- scripts/scan_pdf.py | 303 ++++++++++++ skill.yaml | 2 +- 16 files changed, 2317 insertions(+), 13 deletions(-) create mode 100644 reports/optimization-rerun-20260517/diff.json create mode 100644 reports/optimization-rerun-20260517/diff.txt create mode 100644 reports/optimization-rerun-20260517/pr-summary.md create mode 100644 reports/optimization-rerun-20260517/sit-report.html create mode 100644 reports/optimization-rerun-20260517/sit-report.json create mode 100644 reports/optimization-rerun-20260517/sit-report.md create mode 100644 reports/optimization-rerun-20260517/sit-test-run.txt create mode 100644 reports/optimization-rerun-20260517/validate.txt create mode 100755 scripts/convert_figures.py create mode 100755 scripts/extract_citation.py create mode 100755 scripts/extract_tables.py create mode 100755 scripts/scan_pdf.py diff --git a/SKILL.md b/SKILL.md index 45d7d4d..fa67573 100644 --- a/SKILL.md +++ b/SKILL.md @@ -20,7 +20,9 @@ Two failure modes to actively avoid: 1. Inspect inputs before editing. - Locate paper source, PDF, figures, logos, existing `template.html`, and target `index.html`. - - Run `scripts/scan_paper.py ` when a TeX source exists. + - Run `scripts/scan_paper.py ` when a TeX source exists. Multi-file projects are followed via `\input`/`\include` automatically. + - Run `scripts/scan_pdf.py ` when only a PDF is available; it produces the same shape of inventory (title, authors, abstract, sections, figure/table captions, links). + - Run `scripts/extract_tables.py ` to dump every table (caption, label, header rows, data rows) as JSON. Use this to seed the table ledger instead of eyeballing the .tex. - Build a table ledger before designing: caption, label, section, whether it is main evidence, and whether it must appear fully on the page. - Identify important tables as well as figures; main results, benchmark comparisons, dataset statistics, and ablations usually belong on the page. @@ -32,24 +34,28 @@ Two failure modes to actively avoid: 3. Prepare assets. - Prefer paper-provided figures over generic visuals. - - Convert PDF figures to PNG/SVG-friendly web assets. Use `scripts/convert_figures.sh ` when applicable. + - Convert figures to web assets with `scripts/convert_figures.py ` (the legacy `convert_figures.sh` is now a shim around it). The script handles multi-page PDFs, `.eps`, `.svg` passthrough, raster passthrough, and writes a `figures.manifest.json` that records source→output mapping plus an empty `alt` field per asset for the LLM to fill. - Copy logos and paper PDF into the webpage output folder when useful. - - Rename web assets to stable ASCII filenames. + - Filenames coming out of the manifest are already slug+hash safe; do not rename them again. -4. Design the page around the paper. +4. Generate the citation block. + - Run `scripts/extract_citation.py [--pdf paper.pdf]` and embed the produced BibTeX as the page's citation default. + - Preserve the leading `% NOTE:` comments verbatim; they document fields that were inferred or guessed (year, venue, authors). When any note is present, surface a "verify before publishing" hint near the citation block on the page rather than silently displaying the draft. + +5. Design the page around the paper. - Derive colors, background, spacing, figure framing, and motifs from the paper's key figures and domain. - First list the paper-specific visual cues, then choose the background. A plain surface, soft paper tone, lab-notebook grid, dark canvas, figure-derived gradient, or no visible texture are all valid; none is the default. - Keep background transitions coherent across sections. Avoid abrupt dark-to-light jumps unless the entire page system intentionally supports that contrast. - Read `references/design_principles.md` when deciding visual style or revising design feedback. -5. Implement the webpage. +6. Implement the webpage. - Produce a self-contained single-page `index.html` unless the repo already has a framework. - Include responsive navigation, resource buttons, figure zoom/modal behavior, and readable tables. - Include full central tables with horizontal scroll on mobile, sticky headers when useful, grouped rows when needed, and highlights for the proposed method or best values. - Use charts only when they clarify a key result beyond the paper figures. - Avoid hidden dependency on the source paper directory; generated page should work from the webpage folder. -6. Validate. +7. Validate. - Run `scripts/check_webpage_links.py ` to verify local `src`/`href` assets. - Reconcile the table ledger against the page: every central table is included fully or has an explicit reason and equivalent representation. - Recheck the design for inherited style artifacts: background, palette, hero layout, and cards should match this paper rather than a prior generated page. @@ -74,6 +80,9 @@ In the final response, include: ## Scripts -- `scripts/scan_paper.py`: summarize TeX title/authors/abstract/sections/figures/tables/links. -- `scripts/convert_figures.sh`: convert one-page PDF figures to PNG with Ghostscript. +- `scripts/scan_paper.py`: summarize TeX title/authors/abstract/sections/figures/tables/links; follows `\input`/`\include`. +- `scripts/scan_pdf.py`: PDF-only inventory in the same shape as `scan_paper.py` (used for `kind: pdf_with_assets`). +- `scripts/extract_tables.py`: dump every LaTeX table (caption/label/header/data rows) as JSON for the table ledger. +- `scripts/extract_citation.py`: produce a best-effort BibTeX draft with explicit notes for unverified fields. +- `scripts/convert_figures.py` (and `convert_figures.sh` shim): convert paper figures to web assets and emit `figures.manifest.json`. Handles multi-page PDFs, `.eps`, `.svg`, raster passthrough, and CJK filenames. - `scripts/check_webpage_links.py`: check local `src`, `href`, and `data-figure` targets in an HTML file. diff --git a/reports/optimization-rerun-20260517/diff.json b/reports/optimization-rerun-20260517/diff.json new file mode 100644 index 0000000..6bf536c --- /dev/null +++ b/reports/optimization-rerun-20260517/diff.json @@ -0,0 +1,125 @@ +{ + "schema_version": "sit.diff.v1", + "old": { + "name": "paper-webpage-builder", + "version": "0.4.0", + "root": "/tmp/paper-webpage-builder-head-20260517232243", + "manifest": "/tmp/paper-webpage-builder-head-20260517232243/skill.yaml", + "source": "/tmp/paper-webpage-builder-head-20260517232243" + }, + "new": { + "name": "paper-webpage-builder", + "version": "0.5.0", + "root": "/mnt/shared-storage-user/xuxinglong-p/paper-webpage-builder", + "manifest": "/mnt/shared-storage-user/xuxinglong-p/paper-webpage-builder/skill.yaml", + "source": "/mnt/shared-storage-user/xuxinglong-p/paper-webpage-builder" + }, + "changed": true, + "breaking": false, + "risk": "review-required", + "suggested_bump": "minor", + "messages": [ + "PACKAGE paper-webpage-builder@0.4.0 -> paper-webpage-builder@0.5.0", + "MANIFEST changed version: '0.4.0' -> '0.5.0'", + "PROMPT changed skill: SKILL.md -> SKILL.md (+17 -8; headings: Paper Webpage Builder, Core Rule, Workflow)", + "SCRIPT added scripts/convert_figures.py (review required; cover with runner or targeted tests)", + "SCRIPT added scripts/extract_citation.py (review required; cover with runner or targeted tests)", + "SCRIPT added scripts/extract_tables.py (review required; cover with runner or targeted tests)", + "SCRIPT added scripts/scan_pdf.py (review required; cover with runner or targeted tests)", + "SCRIPT changed scripts/convert_figures.sh (review required; cover with runner or targeted tests)", + "SCRIPT changed scripts/scan_paper.py (review required; cover with runner or targeted tests)", + "RISK review-required" + ], + "events": [ + { + "category": "package", + "severity": "info", + "changed": false, + "breaking": false, + "message": "PACKAGE paper-webpage-builder@0.4.0 -> paper-webpage-builder@0.5.0" + }, + { + "category": "manifest", + "severity": "changed", + "changed": true, + "breaking": false, + "message": "MANIFEST changed version: '0.4.0' -> '0.5.0'" + }, + { + "category": "prompt", + "severity": "changed", + "changed": true, + "breaking": false, + "message": "PROMPT changed skill: SKILL.md -> SKILL.md (+17 -8; headings: Paper Webpage Builder, Core Rule, Workflow)" + }, + { + "category": "script", + "severity": "changed", + "changed": true, + "breaking": false, + "message": "SCRIPT added scripts/convert_figures.py (review required; cover with runner or targeted tests)" + }, + { + "category": "script", + "severity": "changed", + "changed": true, + "breaking": false, + "message": "SCRIPT added scripts/extract_citation.py (review required; cover with runner or targeted tests)" + }, + { + "category": "script", + "severity": "changed", + "changed": true, + "breaking": false, + "message": "SCRIPT added scripts/extract_tables.py (review required; cover with runner or targeted tests)" + }, + { + "category": "script", + "severity": "changed", + "changed": true, + "breaking": false, + "message": "SCRIPT added scripts/scan_pdf.py (review required; cover with runner or targeted tests)" + }, + { + "category": "script", + "severity": "changed", + "changed": true, + "breaking": false, + "message": "SCRIPT changed scripts/convert_figures.sh (review required; cover with runner or targeted tests)" + }, + { + "category": "script", + "severity": "changed", + "changed": true, + "breaking": false, + "message": "SCRIPT changed scripts/scan_paper.py (review required; cover with runner or targeted tests)" + }, + { + "category": "risk", + "severity": "changed", + "changed": true, + "breaking": false, + "message": "RISK review-required" + } + ], + "text_diffs": [ + { + "kind": "prompt", + "name": "skill", + "old_path": "/tmp/paper-webpage-builder-head-20260517232243/SKILL.md", + "new_path": "/mnt/shared-storage-user/xuxinglong-p/paper-webpage-builder/SKILL.md", + "added_lines": 17, + "removed_lines": 8, + "headings": [ + "Paper Webpage Builder", + "Core Rule", + "Workflow", + "Output Expectations", + "Reference Files", + "Scripts" + ], + "variables": [], + "summary": "PROMPT summary skill: +17 -8; headings: Paper Webpage Builder, Core Rule, Workflow, Output Expectations, Reference Files" + } + ] +} diff --git a/reports/optimization-rerun-20260517/diff.txt b/reports/optimization-rerun-20260517/diff.txt new file mode 100644 index 0000000..32bea6a --- /dev/null +++ b/reports/optimization-rerun-20260517/diff.txt @@ -0,0 +1,90 @@ +Skill Diff +Baseline: paper-webpage-builder@0.4.0 +Current: paper-webpage-builder@0.5.0 +Risk: review-required +Suggested version bump: minor + +[manifest] + - MANIFEST changed version: '0.4.0' -> '0.5.0' + +[package] + - PACKAGE paper-webpage-builder@0.4.0 -> paper-webpage-builder@0.5.0 + +[prompt] + - PROMPT changed skill: SKILL.md -> SKILL.md (+17 -8; headings: Paper Webpage Builder, Core Rule, Workflow) + +[risk] + - RISK review-required + +[script] + - SCRIPT added scripts/convert_figures.py (review required; cover with runner or targeted tests) + - SCRIPT added scripts/extract_citation.py (review required; cover with runner or targeted tests) + - SCRIPT added scripts/extract_tables.py (review required; cover with runner or targeted tests) + - SCRIPT added scripts/scan_pdf.py (review required; cover with runner or targeted tests) + - SCRIPT changed scripts/convert_figures.sh (review required; cover with runner or targeted tests) + - SCRIPT changed scripts/scan_paper.py (review required; cover with runner or targeted tests) + +Prompt/Reference Text Summary: +PROMPT summary skill: +17 -8; headings: Paper Webpage Builder, Core Rule, Workflow, Output Expectations, Reference Files + +Prompt/Reference Unified Diff: +--- prompt: skill +--- SKILL.md ++++ SKILL.md +@@ -20,7 +20,9 @@ + + 1. Inspect inputs before editing. + - Locate paper source, PDF, figures, logos, existing `template.html`, and target `index.html`. +- - Run `scripts/scan_paper.py ` when a TeX source exists. ++ - Run `scripts/scan_paper.py ` when a TeX source exists. Multi-file projects are followed via `\input`/`\include` automatically. ++ - Run `scripts/scan_pdf.py ` when only a PDF is available; it produces the same shape of inventory (title, authors, abstract, sections, figure/table captions, links). ++ - Run `scripts/extract_tables.py ` to dump every table (caption, label, header rows, data rows) as JSON. Use this to seed the table ledger instead of eyeballing the .tex. + - Build a table ledger before designing: caption, label, section, whether it is main evidence, and whether it must appear fully on the page. + - Identify important tables as well as figures; main results, benchmark comparisons, dataset statistics, and ablations usually belong on the page. + +@@ -32,24 +34,28 @@ + + 3. Prepare assets. + - Prefer paper-provided figures over generic visuals. +- - Convert PDF figures to PNG/SVG-friendly web assets. Use `scripts/convert_figures.sh ` when applicable. ++ - Convert figures to web assets with `scripts/convert_figures.py ` (the legacy `convert_figures.sh` is now a shim around it). The script handles multi-page PDFs, `.eps`, `.svg` passthrough, raster passthrough, and writes a `figures.manifest.json` that records source→output mapping plus an empty `alt` field per asset for the LLM to fill. + - Copy logos and paper PDF into the webpage output folder when useful. +- - Rename web assets to stable ASCII filenames. ++ - Filenames coming out of the manifest are already slug+hash safe; do not rename them again. + +-4. Design the page around the paper. ++4. Generate the citation block. ++ - Run `scripts/extract_citation.py [--pdf paper.pdf]` and embed the produced BibTeX as the page's citation default. ++ - Preserve the leading `% NOTE:` comments verbatim; they document fields that were inferred or guessed (year, venue, authors). When any note is present, surface a "verify before publishing" hint near the citation block on the page rather than silently displaying the draft. ++ ++5. Design the page around the paper. + - Derive colors, background, spacing, figure framing, and motifs from the paper's key figures and domain. + - First list the paper-specific visual cues, then choose the background. A plain surface, soft paper tone, lab-notebook grid, dark canvas, figure-derived gradient, or no visible texture are all valid; none is the default. + - Keep background transitions coherent across sections. Avoid abrupt dark-to-light jumps unless the entire page system intentionally supports that contrast. + - Read `references/design_principles.md` when deciding visual style or revising design feedback. + +-5. Implement the webpage. ++6. Implement the webpage. + - Produce a self-contained single-page `index.html` unless the repo already has a framework. + - Include responsive navigation, resource buttons, figure zoom/modal behavior, and readable tables. + - Include full central tables with horizontal scroll on mobile, sticky headers when useful, grouped rows when needed, and highlights for the proposed method or best values. + - Use charts only when they clarify a key result beyond the paper figures. + - Avoid hidden dependency on the source paper directory; generated page should work from the webpage folder. + +-6. Validate. ++7. Validate. + - Run `scripts/check_webpage_links.py ` to verify local `src`/`href` assets. + - Reconcile the table ledger against the page: every central table is included fully or has an explicit reason and equivalent representation. + - Recheck the design for inherited style artifacts: background, palette, hero layout, and cards should match this paper rather than a prior generated page. +@@ -74,6 +80,9 @@ + + ## Scripts + +-- `scripts/scan_paper.py`: summarize TeX title/authors/abstract/sections/figures/tables/links. +-- `scripts/convert_figures.sh`: convert one-page PDF figures to PNG with Ghostscript. ++- `scripts/scan_paper.py`: summarize TeX title/authors/abstract/sections/figures/tables/links; follows `\input`/`\include`. ++- `scripts/scan_pdf.py`: PDF-only inventory in the same shape as `scan_paper.py` (used for `kind: pdf_with_assets`). ++- `scripts/extract_tables.py`: dump every LaTeX table (caption/label/header/data rows) as JSON for the table ledger. ++- `scripts/extract_citation.py`: produce a best-effort BibTeX draft with explicit notes for unverified fields. ++- `scripts/convert_figures.py` (and `convert_figures.sh` shim): convert paper figures to web assets and emit `figures.manifest.json`. Handles multi-page PDFs, `.eps`, `.svg`, raster passthrough, and CJK filenames. + - `scripts/check_webpage_links.py`: check local `src`, `href`, and `data-figure` targets in an HTML file. diff --git a/reports/optimization-rerun-20260517/pr-summary.md b/reports/optimization-rerun-20260517/pr-summary.md new file mode 100644 index 0000000..bede090 --- /dev/null +++ b/reports/optimization-rerun-20260517/pr-summary.md @@ -0,0 +1,33 @@ +## Skill Change Summary + +- Baseline: `paper-webpage-builder@0.4.0` +- Current: `paper-webpage-builder@0.5.0` +- Validation: pass +- Golden tests: pass +- Risk: `review-required` +- Suggested version bump: `minor` + +### Semantic Diff + +- `PACKAGE paper-webpage-builder@0.4.0 -> paper-webpage-builder@0.5.0` +- `MANIFEST changed version: '0.4.0' -> '0.5.0'` +- `PROMPT changed skill: SKILL.md -> SKILL.md (+17 -8; headings: Paper Webpage Builder, Core Rule, Workflow)` +- `SCRIPT added scripts/convert_figures.py (review required; cover with runner or targeted tests)` +- `SCRIPT added scripts/extract_citation.py (review required; cover with runner or targeted tests)` +- `SCRIPT added scripts/extract_tables.py (review required; cover with runner or targeted tests)` +- `SCRIPT added scripts/scan_pdf.py (review required; cover with runner or targeted tests)` +- `SCRIPT changed scripts/convert_figures.sh (review required; cover with runner or targeted tests)` +- `SCRIPT changed scripts/scan_paper.py (review required; cover with runner or targeted tests)` +- `RISK review-required` + +### Prompt/Reference Text Summary + +- `PROMPT summary skill: +17 -8; headings: Paper Webpage Builder, Core Rule, Workflow, Output Expectations, Reference Files` + +### Reproduce + +```bash +sit validate /mnt/shared-storage-user/xuxinglong-p/paper-webpage-builder +sit test /mnt/shared-storage-user/xuxinglong-p/paper-webpage-builder +sit diff /tmp/paper-webpage-builder-head-20260517232243 /mnt/shared-storage-user/xuxinglong-p/paper-webpage-builder +``` diff --git a/reports/optimization-rerun-20260517/sit-report.html b/reports/optimization-rerun-20260517/sit-report.html new file mode 100644 index 0000000..9af1e14 --- /dev/null +++ b/reports/optimization-rerun-20260517/sit-report.html @@ -0,0 +1,430 @@ + + + + + +paper-webpage-builder 0.5.0 SitHub Report + + + +
+
+
+

SitHub semantic control report

+

paper-webpage-builder

+

Version 0.5.0 - 2026-05-18

+
+
+Diff risk +review-required +Suggested bump: minor +
+
+
+
+Validation +pass +
+
+Golden Tests +pass +
+
+Cases Passed +3/3 +
+
+Version Bump +minor +
+
+
+
+

Golden Test Progress

+3/3 +
+
+
+
+
+
+
+

Validation

+
    +
  • OK name: paper-webpage-builder
  • +
  • OK version: 0.5.0
  • +
  • OK manifest exists: /mnt/shared-storage-user/xuxinglong-p/paper-webpage-builder/skill.yaml
  • +
  • OK prompt.skill exists: /mnt/shared-storage-user/xuxinglong-p/paper-webpage-builder/SKILL.md
  • +
  • OK prompt.module_patterns exists: /mnt/shared-storage-user/xuxinglong-p/paper-webpage-builder/references/module_patterns.md
  • +
  • OK prompt.design_principles exists: /mnt/shared-storage-user/xuxinglong-p/paper-webpage-builder/references/design_principles.md
  • +
  • OK schema.input exists: /mnt/shared-storage-user/xuxinglong-p/paper-webpage-builder/schemas/input.schema.json
  • +
  • OK schema.output exists: /mnt/shared-storage-user/xuxinglong-p/paper-webpage-builder/schemas/output.schema.json
  • +
  • OK test.golden exists: /mnt/shared-storage-user/xuxinglong-p/paper-webpage-builder/tests/golden.jsonl
  • +
  • OK schema.input JSON schema valid
  • +
  • OK schema.output JSON schema valid
  • +
  • OK test.golden JSONL parsed: 3 cases
  • +
  • OK command.run_case: configured
  • +
+
+
+

Golden Tests

+
    +
  • OK latex-project-page: partial match passed
  • +
  • OK pdf-with-assets-page: partial match passed
  • +
  • OK existing-webpage-refresh: partial match passed
  • +
  • SUMMARY 3/3 golden cases passed
  • +
+
+
+
+
+

Semantic Diff

+review-required +
+
+ + + + + +
+
+
+
+package +#1 +
+ +

PACKAGE paper-webpage-builder@0.4.0 -> paper-webpage-builder@0.5.0

+
+
+
+manifest +#2 +
+ +

MANIFEST changed version: '0.4.0' -> '0.5.0'

+
+
+
+prompt +#3 +
+ +

PROMPT changed skill: SKILL.md -> SKILL.md (+17 -8; headings: Paper Webpage Builder, Core Rule, Workflow)

+
+
+
+script +#4 +
+ +

SCRIPT added scripts/convert_figures.py (review required; cover with runner or targeted tests)

+
+
+
+script +#5 +
+ +

SCRIPT added scripts/extract_citation.py (review required; cover with runner or targeted tests)

+
+
+
+script +#6 +
+ +

SCRIPT added scripts/extract_tables.py (review required; cover with runner or targeted tests)

+
+
+
+script +#7 +
+ +

SCRIPT added scripts/scan_pdf.py (review required; cover with runner or targeted tests)

+
+
+
+script +#8 +
+ +

SCRIPT changed scripts/convert_figures.sh (review required; cover with runner or targeted tests)

+
+
+
+script +#9 +
+ +

SCRIPT changed scripts/scan_paper.py (review required; cover with runner or targeted tests)

+
+
+
+risk +#10 +
+ +

RISK review-required

+
+
+
+
+
+

Reproduce

+local commands +
+

+python3 -m sit.cli validate /mnt/shared-storage-user/xuxinglong-p/paper-webpage-builder
+python3 -m sit.cli test /mnt/shared-storage-user/xuxinglong-p/paper-webpage-builder
+python3 -m sit.cli diff /tmp/paper-webpage-builder-head-20260517232243 /mnt/shared-storage-user/xuxinglong-p/paper-webpage-builder
+
+
+ +
+ + diff --git a/reports/optimization-rerun-20260517/sit-report.json b/reports/optimization-rerun-20260517/sit-report.json new file mode 100644 index 0000000..b997560 --- /dev/null +++ b/reports/optimization-rerun-20260517/sit-report.json @@ -0,0 +1,171 @@ +{ + "schema_version": "sit.report.v1", + "date": "2026-05-18", + "package": { + "name": "paper-webpage-builder", + "version": "0.5.0", + "description": "Build polished single-page academic project webpages from paper sources, figures, references, and validation scripts.", + "root": "/mnt/shared-storage-user/xuxinglong-p/paper-webpage-builder", + "manifest": "/mnt/shared-storage-user/xuxinglong-p/paper-webpage-builder/skill.yaml" + }, + "validation": { + "status": "pass", + "ok": true, + "messages": [ + "OK name: paper-webpage-builder", + "OK version: 0.5.0", + "OK manifest exists: /mnt/shared-storage-user/xuxinglong-p/paper-webpage-builder/skill.yaml", + "OK prompt.skill exists: /mnt/shared-storage-user/xuxinglong-p/paper-webpage-builder/SKILL.md", + "OK prompt.module_patterns exists: /mnt/shared-storage-user/xuxinglong-p/paper-webpage-builder/references/module_patterns.md", + "OK prompt.design_principles exists: /mnt/shared-storage-user/xuxinglong-p/paper-webpage-builder/references/design_principles.md", + "OK schema.input exists: /mnt/shared-storage-user/xuxinglong-p/paper-webpage-builder/schemas/input.schema.json", + "OK schema.output exists: /mnt/shared-storage-user/xuxinglong-p/paper-webpage-builder/schemas/output.schema.json", + "OK test.golden exists: /mnt/shared-storage-user/xuxinglong-p/paper-webpage-builder/tests/golden.jsonl", + "OK schema.input JSON schema valid", + "OK schema.output JSON schema valid", + "OK test.golden JSONL parsed: 3 cases", + "OK command.run_case: configured" + ] + }, + "golden_tests": { + "status": "pass", + "ok": true, + "passed": 3, + "total": 3, + "summary": "SUMMARY 3/3 golden cases passed", + "messages": [ + "OK latex-project-page: partial match passed", + "OK pdf-with-assets-page: partial match passed", + "OK existing-webpage-refresh: partial match passed", + "SUMMARY 3/3 golden cases passed" + ] + }, + "diff": { + "schema_version": "sit.diff.v1", + "old": { + "name": "paper-webpage-builder", + "version": "0.4.0", + "root": "/tmp/paper-webpage-builder-head-20260517232243", + "manifest": "/tmp/paper-webpage-builder-head-20260517232243/skill.yaml" + }, + "new": { + "name": "paper-webpage-builder", + "version": "0.5.0", + "root": "/mnt/shared-storage-user/xuxinglong-p/paper-webpage-builder", + "manifest": "/mnt/shared-storage-user/xuxinglong-p/paper-webpage-builder/skill.yaml" + }, + "changed": true, + "breaking": false, + "risk": "review-required", + "suggested_bump": "minor", + "messages": [ + "PACKAGE paper-webpage-builder@0.4.0 -> paper-webpage-builder@0.5.0", + "MANIFEST changed version: '0.4.0' -> '0.5.0'", + "PROMPT changed skill: SKILL.md -> SKILL.md (+17 -8; headings: Paper Webpage Builder, Core Rule, Workflow)", + "SCRIPT added scripts/convert_figures.py (review required; cover with runner or targeted tests)", + "SCRIPT added scripts/extract_citation.py (review required; cover with runner or targeted tests)", + "SCRIPT added scripts/extract_tables.py (review required; cover with runner or targeted tests)", + "SCRIPT added scripts/scan_pdf.py (review required; cover with runner or targeted tests)", + "SCRIPT changed scripts/convert_figures.sh (review required; cover with runner or targeted tests)", + "SCRIPT changed scripts/scan_paper.py (review required; cover with runner or targeted tests)", + "RISK review-required" + ], + "events": [ + { + "category": "package", + "severity": "info", + "changed": false, + "breaking": false, + "message": "PACKAGE paper-webpage-builder@0.4.0 -> paper-webpage-builder@0.5.0" + }, + { + "category": "manifest", + "severity": "changed", + "changed": true, + "breaking": false, + "message": "MANIFEST changed version: '0.4.0' -> '0.5.0'" + }, + { + "category": "prompt", + "severity": "changed", + "changed": true, + "breaking": false, + "message": "PROMPT changed skill: SKILL.md -> SKILL.md (+17 -8; headings: Paper Webpage Builder, Core Rule, Workflow)" + }, + { + "category": "script", + "severity": "changed", + "changed": true, + "breaking": false, + "message": "SCRIPT added scripts/convert_figures.py (review required; cover with runner or targeted tests)" + }, + { + "category": "script", + "severity": "changed", + "changed": true, + "breaking": false, + "message": "SCRIPT added scripts/extract_citation.py (review required; cover with runner or targeted tests)" + }, + { + "category": "script", + "severity": "changed", + "changed": true, + "breaking": false, + "message": "SCRIPT added scripts/extract_tables.py (review required; cover with runner or targeted tests)" + }, + { + "category": "script", + "severity": "changed", + "changed": true, + "breaking": false, + "message": "SCRIPT added scripts/scan_pdf.py (review required; cover with runner or targeted tests)" + }, + { + "category": "script", + "severity": "changed", + "changed": true, + "breaking": false, + "message": "SCRIPT changed scripts/convert_figures.sh (review required; cover with runner or targeted tests)" + }, + { + "category": "script", + "severity": "changed", + "changed": true, + "breaking": false, + "message": "SCRIPT changed scripts/scan_paper.py (review required; cover with runner or targeted tests)" + }, + { + "category": "risk", + "severity": "changed", + "changed": true, + "breaking": false, + "message": "RISK review-required" + } + ], + "text_diffs": [ + { + "kind": "prompt", + "name": "skill", + "old_path": "/tmp/paper-webpage-builder-head-20260517232243/SKILL.md", + "new_path": "/mnt/shared-storage-user/xuxinglong-p/paper-webpage-builder/SKILL.md", + "added_lines": 17, + "removed_lines": 8, + "headings": [ + "Paper Webpage Builder", + "Core Rule", + "Workflow", + "Output Expectations", + "Reference Files", + "Scripts" + ], + "variables": [], + "summary": "PROMPT summary skill: +17 -8; headings: Paper Webpage Builder, Core Rule, Workflow, Output Expectations, Reference Files" + } + ] + }, + "reproducibility": { + "validate": "python3 -m sit.cli validate /mnt/shared-storage-user/xuxinglong-p/paper-webpage-builder", + "test": "python3 -m sit.cli test /mnt/shared-storage-user/xuxinglong-p/paper-webpage-builder", + "diff": "python3 -m sit.cli diff /tmp/paper-webpage-builder-head-20260517232243 /mnt/shared-storage-user/xuxinglong-p/paper-webpage-builder" + } +} diff --git a/reports/optimization-rerun-20260517/sit-report.md b/reports/optimization-rerun-20260517/sit-report.md new file mode 100644 index 0000000..05fb688 --- /dev/null +++ b/reports/optimization-rerun-20260517/sit-report.md @@ -0,0 +1,60 @@ +# paper-webpage-builder 0.5.0 SIT Report + +Date: 2026-05-18 + +## Package + +- Name: `paper-webpage-builder` +- Version: `0.5.0` +- Root: `/mnt/shared-storage-user/xuxinglong-p/paper-webpage-builder` +- Manifest: `/mnt/shared-storage-user/xuxinglong-p/paper-webpage-builder/skill.yaml` +- Description: Build polished single-page academic project webpages from paper sources, figures, references, and validation scripts. + +## Validation + +- Result: pass +- `OK name: paper-webpage-builder` +- `OK version: 0.5.0` +- `OK manifest exists: /mnt/shared-storage-user/xuxinglong-p/paper-webpage-builder/skill.yaml` +- `OK prompt.skill exists: /mnt/shared-storage-user/xuxinglong-p/paper-webpage-builder/SKILL.md` +- `OK prompt.module_patterns exists: /mnt/shared-storage-user/xuxinglong-p/paper-webpage-builder/references/module_patterns.md` +- `OK prompt.design_principles exists: /mnt/shared-storage-user/xuxinglong-p/paper-webpage-builder/references/design_principles.md` +- `OK schema.input exists: /mnt/shared-storage-user/xuxinglong-p/paper-webpage-builder/schemas/input.schema.json` +- `OK schema.output exists: /mnt/shared-storage-user/xuxinglong-p/paper-webpage-builder/schemas/output.schema.json` +- `OK test.golden exists: /mnt/shared-storage-user/xuxinglong-p/paper-webpage-builder/tests/golden.jsonl` +- `OK schema.input JSON schema valid` +- `OK schema.output JSON schema valid` +- `OK test.golden JSONL parsed: 3 cases` +- `OK command.run_case: configured` + +## Golden Tests + +- Result: pass +- `OK latex-project-page: partial match passed` +- `OK pdf-with-assets-page: partial match passed` +- `OK existing-webpage-refresh: partial match passed` +- `SUMMARY 3/3 golden cases passed` + +## Diff + +- Baseline: `paper-webpage-builder@0.4.0` +- Current: `paper-webpage-builder@0.5.0` +- `PACKAGE paper-webpage-builder@0.4.0 -> paper-webpage-builder@0.5.0` +- `MANIFEST changed version: '0.4.0' -> '0.5.0'` +- `PROMPT changed skill: SKILL.md -> SKILL.md (+17 -8; headings: Paper Webpage Builder, Core Rule, Workflow)` +- `SCRIPT added scripts/convert_figures.py (review required; cover with runner or targeted tests)` +- `SCRIPT added scripts/extract_citation.py (review required; cover with runner or targeted tests)` +- `SCRIPT added scripts/extract_tables.py (review required; cover with runner or targeted tests)` +- `SCRIPT added scripts/scan_pdf.py (review required; cover with runner or targeted tests)` +- `SCRIPT changed scripts/convert_figures.sh (review required; cover with runner or targeted tests)` +- `SCRIPT changed scripts/scan_paper.py (review required; cover with runner or targeted tests)` +- `RISK review-required` + +## Reproducibility + +- Re-run validation with: + `python3 -m sit.cli validate /mnt/shared-storage-user/xuxinglong-p/paper-webpage-builder` +- Re-run golden schema tests with: + `python3 -m sit.cli test /mnt/shared-storage-user/xuxinglong-p/paper-webpage-builder` +- Re-run package diff with: + `python3 -m sit.cli diff /tmp/paper-webpage-builder-head-20260517232243 /mnt/shared-storage-user/xuxinglong-p/paper-webpage-builder` diff --git a/reports/optimization-rerun-20260517/sit-test-run.txt b/reports/optimization-rerun-20260517/sit-test-run.txt new file mode 100644 index 0000000..710f0ba --- /dev/null +++ b/reports/optimization-rerun-20260517/sit-test-run.txt @@ -0,0 +1,10 @@ +Skill Tests +Result: pass + +OK latex-project-page: runner produced actual +OK latex-project-page: partial match passed +OK pdf-with-assets-page: runner produced actual +OK pdf-with-assets-page: partial match passed +OK existing-webpage-refresh: runner produced actual +OK existing-webpage-refresh: partial match passed +SUMMARY 3/3 golden cases passed diff --git a/reports/optimization-rerun-20260517/validate.txt b/reports/optimization-rerun-20260517/validate.txt new file mode 100644 index 0000000..7f07aaa --- /dev/null +++ b/reports/optimization-rerun-20260517/validate.txt @@ -0,0 +1,13 @@ +OK name: paper-webpage-builder +OK version: 0.5.0 +OK manifest exists: /mnt/shared-storage-user/xuxinglong-p/paper-webpage-builder/skill.yaml +OK prompt.skill exists: /mnt/shared-storage-user/xuxinglong-p/paper-webpage-builder/SKILL.md +OK prompt.module_patterns exists: /mnt/shared-storage-user/xuxinglong-p/paper-webpage-builder/references/module_patterns.md +OK prompt.design_principles exists: /mnt/shared-storage-user/xuxinglong-p/paper-webpage-builder/references/design_principles.md +OK schema.input exists: /mnt/shared-storage-user/xuxinglong-p/paper-webpage-builder/schemas/input.schema.json +OK schema.output exists: /mnt/shared-storage-user/xuxinglong-p/paper-webpage-builder/schemas/output.schema.json +OK test.golden exists: /mnt/shared-storage-user/xuxinglong-p/paper-webpage-builder/tests/golden.jsonl +OK schema.input JSON schema valid +OK schema.output JSON schema valid +OK test.golden JSONL parsed: 3 cases +OK command.run_case: configured diff --git a/scripts/convert_figures.py b/scripts/convert_figures.py new file mode 100755 index 0000000..8df9bce --- /dev/null +++ b/scripts/convert_figures.py @@ -0,0 +1,335 @@ +#!/usr/bin/env python3 +"""Convert paper figures into web-ready files with a stable manifest. + +Improvements over the original `convert_figures.sh`: + * Multi-page PDFs produce one PNG per page (`name-p1.png`, `name-p2.png`) + so subfigure-pack PDFs are not silently truncated to page 1. + * `.eps` is rasterised through Ghostscript when available. + * `.svg` is copied as-is (browsers render SVG natively). + * `.png` / `.jpg` / `.jpeg` / `.webp` / `.gif` pass through with renaming. + * Filenames are slugified ASCII; CJK / accented names get a deterministic + 8-char hash suffix so distinct sources never collide on the same slug. + * A `figures.manifest.json` is written next to the converted assets, + mapping each output file to its source path, page index, dimensions + when known, and a placeholder `alt` ready for the LLM to fill in. + +Usage: + convert_figures.py [--dpi 180] + [--manifest ] [--no-multipage] + +Exit codes: + 0 conversions ran (manifest may still list per-file errors) + 2 bad arguments / source dir missing +""" + +from __future__ import annotations + +import argparse +import hashlib +import json +import re +import shutil +import subprocess +import sys +import unicodedata +from dataclasses import asdict, dataclass, field +from pathlib import Path +from typing import Iterable + +RASTER_EXTS = {".png", ".jpg", ".jpeg", ".webp", ".gif"} +VECTOR_PASSTHROUGH_EXTS = {".svg"} +PDF_EXTS = {".pdf"} +EPS_EXTS = {".eps", ".ps"} + + +def have(tool: str) -> bool: + return shutil.which(tool) is not None + + +def slugify(name: str) -> tuple[str, bool]: + """Return (slug, hashed). hashed=True if non-ASCII content was lost.""" + original = name + nfkd = unicodedata.normalize("NFKD", name) + ascii_only = nfkd.encode("ascii", "ignore").decode("ascii") + slug = re.sub(r"[^A-Za-z0-9]+", "-", ascii_only).strip("-").lower() + hashed = False + if not slug or slug != re.sub(r"[^A-Za-z0-9]+", "-", original).strip("-").lower(): + digest = hashlib.sha1(original.encode("utf-8")).hexdigest()[:8] + slug = (slug or "figure") + slug = f"{slug}-{digest}" + hashed = True + return slug, hashed + + +@dataclass +class ConvertedAsset: + source: str + output: str + kind: str + page: int | None = None + width: int | None = None + height: int | None = None + alt: str = "" + notes: list[str] = field(default_factory=list) + + +def gather_sources(src_dir: Path) -> list[Path]: + sources: list[Path] = [] + for path in sorted(src_dir.iterdir()): + if not path.is_file() or path.name.startswith("._") or path.name.startswith("."): + continue + ext = path.suffix.lower() + if ext in RASTER_EXTS | VECTOR_PASSTHROUGH_EXTS | PDF_EXTS | EPS_EXTS: + sources.append(path) + return sources + + +def rasterize_pdf_with_pymupdf( + src: Path, dst_dir: Path, slug: str, dpi: int, multipage: bool +) -> tuple[list[Path], list[ConvertedAsset]]: + import fitz # type: ignore + + outputs: list[Path] = [] + assets: list[ConvertedAsset] = [] + zoom = dpi / 72 + matrix = fitz.Matrix(zoom, zoom) + with fitz.open(src) as doc: + page_count = doc.page_count + pages = range(page_count) if multipage else range(min(1, page_count)) + for index in pages: + page = doc[index] + pix = page.get_pixmap(matrix=matrix, alpha=True) + if page_count > 1 and multipage: + out = unique_path(dst_dir, f"{slug}-p{index + 1}.png") + else: + out = unique_path(dst_dir, f"{slug}.png") + pix.save(out) + outputs.append(out) + assets.append( + ConvertedAsset( + source=str(src), + output=str(out), + kind="raster-from-pdf", + page=index + 1 if page_count > 1 else None, + width=pix.width, + height=pix.height, + ) + ) + return outputs, assets + + +def rasterize_pdf_with_gs( + src: Path, dst_dir: Path, slug: str, dpi: int, multipage: bool +) -> tuple[list[Path], list[ConvertedAsset]]: + if not have("gs"): + raise RuntimeError("Ghostscript missing; cannot rasterize PDF without pymupdf either.") + page_count = 1 + if have("pdfinfo"): + try: + info = subprocess.run( + ["pdfinfo", str(src)], check=True, capture_output=True, text=True + ).stdout + for line in info.splitlines(): + if line.lower().startswith("pages:"): + page_count = int(line.split(":")[1].strip()) + break + except Exception: + page_count = 1 + last_page = page_count if multipage else 1 + outputs: list[Path] = [] + assets: list[ConvertedAsset] = [] + + if page_count == 1 or not multipage: + out = unique_path(dst_dir, f"{slug}.png") + cmd = [ + "gs", "-dSAFER", "-dBATCH", "-dNOPAUSE", "-sDEVICE=pngalpha", + f"-r{dpi}", "-dFirstPage=1", "-dLastPage=1", + f"-sOutputFile={out}", str(src), + ] + result = subprocess.run(cmd, capture_output=True) + if result.returncode == 0 and out.exists(): + outputs.append(out) + assets.append(ConvertedAsset(source=str(src), output=str(out), kind="raster-from-pdf")) + return outputs, assets + + pattern_out = unique_path(dst_dir, f"{slug}-p%d.png") + cmd = [ + "gs", "-dSAFER", "-dBATCH", "-dNOPAUSE", "-sDEVICE=pngalpha", + f"-r{dpi}", "-dFirstPage=1", f"-dLastPage={last_page}", + f"-sOutputFile={pattern_out}", str(src), + ] + subprocess.run(cmd, capture_output=True) + for index in range(1, last_page + 1): + out = pattern_out.with_name(pattern_out.name.replace("%d", str(index))) + if out.exists(): + outputs.append(out) + assets.append( + ConvertedAsset( + source=str(src), output=str(out), kind="raster-from-pdf", + page=index, + ) + ) + return outputs, assets + + +def rasterize_eps(src: Path, dst_dir: Path, slug: str, dpi: int) -> tuple[list[Path], list[ConvertedAsset]]: + if not have("gs"): + return [], [ConvertedAsset( + source=str(src), output="", kind="eps", + notes=["skipped: ghostscript missing"], + )] + out = unique_path(dst_dir, f"{slug}.png") + cmd = [ + "gs", "-dSAFER", "-dBATCH", "-dNOPAUSE", "-dEPSCrop", + "-sDEVICE=pngalpha", f"-r{dpi}", f"-sOutputFile={out}", str(src), + ] + result = subprocess.run(cmd, capture_output=True) + if result.returncode != 0 or not out.exists(): + return [], [ConvertedAsset( + source=str(src), output="", kind="eps", + notes=[f"ghostscript failed: {result.stderr.decode('utf-8', 'replace')[:200]}"], + )] + return [out], [ConvertedAsset(source=str(src), output=str(out), kind="raster-from-eps")] + + +def passthrough(src: Path, dst_dir: Path, slug: str, kind: str) -> tuple[list[Path], list[ConvertedAsset]]: + out = unique_path(dst_dir, f"{slug}{src.suffix.lower()}") + shutil.copy2(src, out) + width, height = (None, None) + if src.suffix.lower() in RASTER_EXTS: + width, height = read_raster_dims(out) + return [out], [ConvertedAsset( + source=str(src), output=str(out), kind=kind, + width=width, height=height, + )] + + +def read_raster_dims(path: Path) -> tuple[int | None, int | None]: + try: + import fitz # type: ignore + doc = fitz.open(path) + try: + pix = doc[0].get_pixmap(alpha=False) + return pix.width, pix.height + finally: + doc.close() + except Exception: + pass + # PNG header probe (no extra deps) + try: + with path.open("rb") as fh: + header = fh.read(24) + if path.suffix.lower() == ".png" and header[:8] == b"\x89PNG\r\n\x1a\n": + width = int.from_bytes(header[16:20], "big") + height = int.from_bytes(header[20:24], "big") + return width, height + except Exception: + return None, None + return None, None + + +def unique_path(directory: Path, filename: str) -> Path: + candidate = directory / filename + if not candidate.exists(): + return candidate + stem = candidate.stem + suffix = candidate.suffix + counter = 2 + while True: + candidate = directory / f"{stem}-{counter}{suffix}" + if not candidate.exists(): + return candidate + counter += 1 + + +def convert_one( + src: Path, dst_dir: Path, dpi: int, multipage: bool +) -> list[ConvertedAsset]: + slug, hashed = slugify(src.stem) + ext = src.suffix.lower() + notes: list[str] = [] + if hashed: + notes.append("non-ASCII source name; appended sha1 suffix to keep filenames stable") + + try: + if ext in PDF_EXTS: + try: + import fitz # noqa: F401 + outputs, assets = rasterize_pdf_with_pymupdf(src, dst_dir, slug, dpi, multipage) + except Exception: + outputs, assets = rasterize_pdf_with_gs(src, dst_dir, slug, dpi, multipage) + elif ext in EPS_EXTS: + outputs, assets = rasterize_eps(src, dst_dir, slug, dpi) + elif ext in VECTOR_PASSTHROUGH_EXTS: + outputs, assets = passthrough(src, dst_dir, slug, "vector") + elif ext in RASTER_EXTS: + outputs, assets = passthrough(src, dst_dir, slug, "raster") + else: + return [ConvertedAsset( + source=str(src), output="", kind="unsupported", + notes=notes + [f"unsupported extension {ext}"], + )] + except Exception as exc: + return [ConvertedAsset( + source=str(src), output="", kind=ext.lstrip("."), + notes=notes + [f"conversion failed: {exc}"], + )] + for asset in assets: + asset.notes.extend(notes) + return assets + + +def write_manifest(target_dir: Path, name: str, assets: Iterable[ConvertedAsset]) -> Path: + payload = { + "version": 1, + "directory": str(target_dir.resolve()), + "assets": [asdict(asset) for asset in assets], + } + out = target_dir / name + out.write_text(json.dumps(payload, indent=2, ensure_ascii=False) + "\n", encoding="utf-8") + return out + + +def main() -> int: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("source_dir", type=Path) + parser.add_argument("target_dir", type=Path) + parser.add_argument("--dpi", type=int, default=180) + parser.add_argument("--manifest", default="figures.manifest.json") + parser.add_argument( + "--no-multipage", + action="store_true", + help="Only export the first page from multi-page PDFs (legacy behaviour).", + ) + args = parser.parse_args() + + if not args.source_dir.is_dir(): + print(f"error: not a directory: {args.source_dir}", file=sys.stderr) + return 2 + args.target_dir.mkdir(parents=True, exist_ok=True) + + sources = gather_sources(args.source_dir) + if not sources: + print(f"warning: no convertible figures found in {args.source_dir}", file=sys.stderr) + + multipage = not args.no_multipage + all_assets: list[ConvertedAsset] = [] + for src in sources: + assets = convert_one(src, args.target_dir, args.dpi, multipage) + for asset in assets: + if asset.output: + print(asset.output) + else: + print( + f"warning: failed to convert {asset.source}: {'; '.join(asset.notes) or 'no output'}", + file=sys.stderr, + ) + all_assets.append(asset) + + manifest_path = write_manifest(args.target_dir, args.manifest, all_assets) + print(f"manifest: {manifest_path}", file=sys.stderr) + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/scripts/convert_figures.sh b/scripts/convert_figures.sh index d058ca0..fdf0828 100755 --- a/scripts/convert_figures.sh +++ b/scripts/convert_figures.sh @@ -1,17 +1,35 @@ #!/usr/bin/env bash +# Compatibility shim for older callers; delegates to convert_figures.py +# which handles multi-page PDFs, .eps/.svg, raster passthrough, and a +# JSON manifest. Pass `--legacy` to force the original page-1-only +# Ghostscript loop preserved at the bottom of this file. + set -euo pipefail if [ "$#" -lt 2 ]; then - echo "Usage: convert_figures.sh [dpi]" >&2 + echo "Usage: convert_figures.sh [dpi] [--legacy]" >&2 exit 2 fi src_dir="$1" dst_dir="$2" dpi="${3:-180}" +legacy=0 +for arg in "$@"; do + if [ "$arg" = "--legacy" ]; then + legacy=1 + fi +done -mkdir -p "$dst_dir" +script_dir="$(cd "$(dirname "$0")" && pwd)" +if [ "$legacy" -eq 0 ] && [ -x "$script_dir/convert_figures.py" ]; then + exec python3 "$script_dir/convert_figures.py" "$src_dir" "$dst_dir" --dpi "$dpi" +fi + +# --- Legacy fallback (page-1-only PDF rasterisation) ----------------------- + +mkdir -p "$dst_dir" if ! command -v gs >/dev/null 2>&1; then echo "Ghostscript (gs) is required to convert PDF figures." >&2 exit 1 diff --git a/scripts/extract_citation.py b/scripts/extract_citation.py new file mode 100755 index 0000000..de13d1c --- /dev/null +++ b/scripts/extract_citation.py @@ -0,0 +1,267 @@ +#!/usr/bin/env python3 +"""Build a best-effort BibTeX draft for the paper itself. + +Reads the root .tex (expanding `\\input/\\include`) plus any sibling `.bib` +files referenced via `\\bibliography{...}` or `\\addbibresource{...}` and +emits a single BibTeX entry on stdout. + +Always includes a leading comment listing what was inferred and what was +guessed, so downstream HTML generation can decide whether to surface a +"venue/year unverified" warning next to the bibtex block. + +Sources of evidence, in order: + 1. arXiv URL — if `arxiv.org/abs/` appears in the paper, year/venue are derived. + 2. \\title{...} / \\author{...} / \\date{...} from the root tex. + 3. PDF metadata — when `--pdf ` is provided and pymupdf is available. +""" + +from __future__ import annotations + +import argparse +import re +import sys +from datetime import datetime, timezone +from pathlib import Path + +INPUT_RE = re.compile(r"\\(?:input|include|subfile)\{([^{}]+)\}") +BIB_RE = re.compile(r"\\(?:bibliography|addbibresource)\{([^{}]+)\}") +ARXIV_RE = re.compile(r"arxiv\.org/(?:abs|pdf)/(\d{4}\.\d{4,5})(?:v\d+)?", re.IGNORECASE) + + +def strip_comments(text: str) -> str: + lines = [] + for line in text.splitlines(): + if line.lstrip().startswith("%"): + lines.append("") + continue + lines.append(re.sub(r"(? str: + try: + return path.read_text(encoding="utf-8") + except UnicodeDecodeError: + return path.read_text(encoding="latin-1", errors="replace") + + +def read_with_inputs(root: Path, max_depth: int = 6) -> str: + visited: set[Path] = set() + chunks: list[str] = [] + + def load(path: Path, depth: int) -> None: + try: + resolved = path.resolve() + except OSError: + return + if resolved in visited or depth > max_depth or not resolved.is_file(): + return + visited.add(resolved) + text = strip_comments(read_text(resolved)) + chunks.append(text) + for ref in INPUT_RE.findall(text): + candidate = resolved.parent / ref.strip() + if candidate.suffix == "": + candidate = candidate.with_suffix(".tex") + load(candidate, depth + 1) + + load(root, 0) + return "\n".join(chunks) + + +def find_braced(text: str, command: str) -> str | None: + pattern = re.compile(r"\\" + re.escape(command) + r"\*?(?:\[[^\]]*\])?\{") + match = pattern.search(text) + if not match: + return None + start = match.end() + depth = 1 + i = start + while i < len(text) and depth: + c = text[i] + if c == "\\" and i + 1 < len(text): + i += 2 + continue + if c == "{": + depth += 1 + elif c == "}": + depth -= 1 + if depth == 0: + return text[start:i] + i += 1 + return None + + +def clean(value: str | None) -> str: + if not value: + return "" + value = re.sub(r"\\(?:textbf|textit|textsc|texttt|emph|mathrm|mathbf)\*?\{([^{}]*)\}", r"\1", value) + value = re.sub(r"\\(?:thanks|footnote|orcidlink)\{[^{}]*\}", "", value) + value = re.sub(r"\\and\b", " and ", value) + value = re.sub(r"\\\\", " ", value) + value = re.sub(r"\\,", " ", value) + value = re.sub(r"\\[a-zA-Z@]+\*?", "", value) + value = value.replace("{", "").replace("}", "") + value = re.sub(r"~", " ", value) + value = re.sub(r"\s+", " ", value).strip(" ,;") + return value + + +def split_authors(raw: str) -> list[str]: + if not raw: + return [] + parts = re.split(r"\band\b|,(?=[^,]+,)|\\and|\n", raw, flags=re.IGNORECASE) + out: list[str] = [] + for p in parts: + p = clean(p) + if not p: + continue + # Drop trailing affiliations / superscripts + p = re.sub(r"[\d\*†‡§¶]+$", "", p).strip() + if 2 <= len(p) <= 80 and re.search(r"[A-Za-z]", p): + out.append(p) + seen: list[str] = [] + for n in out: + if n not in seen: + seen.append(n) + return seen + + +def slug_for_key(first_author: str, year: str, title: str) -> str: + surname = first_author.split()[-1] if first_author else "anon" + surname = re.sub(r"[^A-Za-z]", "", surname).lower() or "anon" + word = "" + for w in title.split(): + cleaned = re.sub(r"[^A-Za-z]", "", w).lower() + if len(cleaned) >= 4 and cleaned not in {"with", "from", "into", "this", "that", "what", "when", "your"}: + word = cleaned + break + if not word: + word = "paper" + return f"{surname}{year or 'XXXX'}{word}" + + +def maybe_pdf_meta(path: Path | None) -> dict[str, str]: + if not path or not path.is_file(): + return {} + try: + import fitz # type: ignore + except Exception: + return {} + try: + with fitz.open(path) as doc: + return {k: v for k, v in (doc.metadata or {}).items() if v} + except Exception: + return {} + + +def main() -> int: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("paper_tex", type=Path, help="Root .tex file (or empty path '-' to skip).") + parser.add_argument("--pdf", type=Path, default=None, help="Optional paper PDF for metadata fallback.") + parser.add_argument("--bib-key", default=None, help="Override the generated cite key.") + args = parser.parse_args() + + notes: list[str] = [] + title = "" + authors: list[str] = [] + year = "" + arxiv_id = "" + bib_files: list[Path] = [] + + text = "" + tex_path = args.paper_tex + if tex_path and str(tex_path) != "-" and tex_path.is_file(): + text = read_with_inputs(tex_path) + title = clean(find_braced(text, "title")) or title + author_blob = find_braced(text, "author") + if author_blob: + authors = split_authors(author_blob) + date_raw = clean(find_braced(text, "date")) + if date_raw: + ymatch = re.search(r"(19|20)\d{2}", date_raw) + if ymatch: + year = ymatch.group(0) + # arXiv + amatch = ARXIV_RE.search(text) + if amatch: + arxiv_id = amatch.group(1) + # collect referenced .bib files + for ref in BIB_RE.findall(text): + for piece in ref.split(","): + piece = piece.strip() + if not piece: + continue + if not piece.endswith(".bib"): + piece = piece + ".bib" + candidate = (tex_path.resolve().parent / piece).resolve() + if candidate.is_file() and candidate not in bib_files: + bib_files.append(candidate) + + # PDF metadata fallback for missing fields. + if not (title and authors and year): + meta = maybe_pdf_meta(args.pdf) + if meta: + if not title and meta.get("title"): + title = meta["title"].strip() + if not authors and meta.get("author"): + authors = split_authors(meta["author"]) + if not year: + for k in ("creationDate", "modDate"): + raw = meta.get(k, "") + ym = re.search(r"D:(\d{4})", raw) + if ym: + year = ym.group(1) + break + + if arxiv_id and not year: + # arXiv id 1701.xxxxx → 2017 + prefix = arxiv_id[:2] + year = ("20" + prefix) if prefix.isdigit() and int(prefix) >= 7 else year + + if not year: + notes.append("year missing — placeholder XXXX written; verify before publishing") + year_value = "XXXX" + else: + year_value = year + + if not title: + notes.append("title missing — placeholder used; verify before publishing") + title = "Unknown Title" + if not authors: + notes.append("authors missing — placeholder used; verify before publishing") + authors = ["Unknown"] + if not arxiv_id: + notes.append("no arXiv id detected — venue assumed unpublished/preprint") + + cite_key = args.bib_key or slug_for_key(authors[0], year, title) + + fields: list[tuple[str, str]] = [ + ("title", "{" + title + "}"), + ("author", " and ".join(authors)), + ("year", year_value), + ] + if arxiv_id: + fields.append(("eprint", arxiv_id)) + fields.append(("archivePrefix", "arXiv")) + fields.append(("url", f"https://arxiv.org/abs/{arxiv_id}")) + entry_type = "@misc" + else: + entry_type = "@unpublished" + + print(f"% Draft generated {datetime.now(timezone.utc).strftime('%Y-%m-%d')} by extract_citation.py") + for note in notes: + print(f"% NOTE: {note}") + if bib_files: + print(f"% bib files referenced by the paper: {', '.join(str(p) for p in bib_files)}") + print() + print(f"{entry_type}{{{cite_key},") + for index, (k, v) in enumerate(fields): + comma = "," if index < len(fields) - 1 else "" + print(f" {k} = {{{v}}}{comma}") + print("}") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/scripts/extract_tables.py b/scripts/extract_tables.py new file mode 100755 index 0000000..2cb3592 --- /dev/null +++ b/scripts/extract_tables.py @@ -0,0 +1,387 @@ +#!/usr/bin/env python3 +"""Extract every LaTeX table into a JSON ledger for the central-table check. + +Emits one JSON document with a list of entries, each containing: +- file: source .tex file (relative to root) +- line: line number in that file +- environment: `table`, `table*`, `longtable`, ... +- label: contents of `\\label{...}` (or null) +- caption: cleaned `\\caption{...}` text (or null) +- column_spec: the alignment spec `\\begin{tabular}{...}` (or null) +- header_rows / data_rows: lists of cell-lists, surface text only +- raw_rows: number of `\\\\` separated rows including header +- notes: list of warning strings (e.g. detected \\multicolumn / \\multirow) + +Designed so an LLM (or a downstream check) can reconcile the page against +every table in the paper without re-reading the .tex itself. +""" + +from __future__ import annotations + +import argparse +import json +import re +import sys +from dataclasses import dataclass, field +from pathlib import Path + +INPUT_RE = re.compile(r"\\(?:input|include|subfile)\{([^{}]+)\}") +TABLE_ENVS = ("table", "table*", "longtable", "sidewaystable", "table**") +ROW_TABULAR_ENV_RE = re.compile( + r"\\begin\{(tabular|tabular\*|tabularx|tabulary|array|longtable)\}" + r"(?:\[[^\]]*\])?" # optional positional arg + r"(?:\{[^{}]*\})?" # optional width arg + r"\{([^{}]*)\}" # column spec + r"(.*?)" + r"\\end\{\1\}", + re.S, +) +RULE_RE = re.compile( + r"\\(?:toprule|midrule|bottomrule|hline|cline\{[^{}]*\}|specialrule\{[^{}]*\}\{[^{}]*\}\{[^{}]*\})" +) +COMMENT_LINE_RE = re.compile(r"(? str: + out_lines: list[str] = [] + for line in text.splitlines(): + if line.lstrip().startswith("%"): + out_lines.append("") + continue + out_lines.append(COMMENT_LINE_RE.sub("", line)) + return "\n".join(out_lines) + + +@dataclass +class Source: + path: Path + text: str + # Map (offset_in_combined, offset_in_combined+len) → (path, line_in_path) + offset: int = 0 + + +def read_with_inputs(root: Path, max_depth: int = 6) -> list[Source]: + """Read root tex, expand \\input/\\include into a flat per-file list.""" + visited: set[Path] = set() + sources: list[Source] = [] + + def load(path: Path, depth: int) -> None: + try: + resolved = path.resolve() + except OSError: + return + if resolved in visited or depth > max_depth or not resolved.is_file(): + return + visited.add(resolved) + try: + raw = resolved.read_text(encoding="utf-8") + except UnicodeDecodeError: + raw = resolved.read_text(encoding="latin-1", errors="replace") + print( + f"warning: {resolved} is not utf-8, decoded as latin-1", + file=sys.stderr, + ) + text = strip_comments(raw) + sources.append(Source(path=resolved, text=text)) + for match in INPUT_RE.finditer(text): + ref = match.group(1).strip() + if not ref: + continue + candidate = (resolved.parent / ref) + if candidate.suffix == "": + candidate = candidate.with_suffix(".tex") + load(candidate, depth + 1) + + load(root, 0) + return sources + + +def find_braced_arg(text: str, command: str) -> str | None: + pattern = re.compile(r"\\" + re.escape(command) + r"\*?(?:\[[^\]]*\])?\{") + match = pattern.search(text) + if not match: + return None + start = match.end() + depth = 1 + i = start + while i < len(text) and depth: + c = text[i] + if c == "\\" and i + 1 < len(text): + i += 2 + continue + if c == "{": + depth += 1 + elif c == "}": + depth -= 1 + if depth == 0: + return text[start:i] + i += 1 + return None + + +def clean_text(value: str) -> str: + if value is None: + return "" + value = value.replace(r"\&", "&").replace(r"\%", "%").replace(r"\_", "_") + value = value.replace(r"\#", "#").replace(r"\$", "$") + value = value.replace(r"\textbackslash", "\\") + # textbf{x} / textsc{x} / emph{x} / mathrm{x}: keep inner. + inner_re = re.compile( + r"\\(?:textbf|textit|textsc|texttt|emph|mathbf|mathrm|mathit|operatorname|" + r"textrm|textsf|underline|uline|bm)\*?\{((?:[^{}]|\{[^{}]*\})*)\}" + ) + for _ in range(4): + new = inner_re.sub(r"\1", value) + if new == value: + break + value = new + # Replace \cite/\ref/\label with placeholders so they don't pollute cells. + value = re.sub(r"\\cite[a-zA-Z]*\*?\{[^{}]*\}", "[cite]", value) + value = re.sub(r"\\ref\*?\{[^{}]*\}", "[ref]", value) + value = re.sub(r"\\label\{[^{}]*\}", "", value) + value = re.sub(r"\\(?:checkmark|xmark|cmark)\b", "✓", value) + value = LATEX_CMD_RE.sub("", value) + value = value.replace("{", "").replace("}", "") + value = re.sub(r"\$([^$]*)\$", r"\1", value) + value = re.sub(r"~", " ", value) + value = re.sub(r"\s+", " ", value).strip() + return value + + +def split_top_level_cells(row: str) -> list[str]: + cells: list[str] = [] + depth = 0 + buf: list[str] = [] + i = 0 + while i < len(row): + c = row[i] + if c == "\\" and i + 1 < len(row): + buf.append(row[i:i + 2]) + i += 2 + continue + if c == "{": + depth += 1 + buf.append(c) + elif c == "}": + depth -= 1 + buf.append(c) + elif c == "&" and depth == 0: + cells.append("".join(buf)) + buf = [] + else: + buf.append(c) + i += 1 + cells.append("".join(buf)) + return cells + + +def expand_multicolumn(cell: str) -> tuple[str, int]: + """Return (visible text, span). Non-multicol cells span 1.""" + m = MULTICOL_RE.search(cell) + if m: + return clean_text(m.group(2)), int(m.group(1)) + m2 = MULTIROW_RE.search(cell) + if m2: + return clean_text(m2.group(2)), 1 + return clean_text(cell), 1 + + +def split_rows(body: str) -> list[str]: + body = RULE_RE.sub("", body) + raw_rows = re.split(r"\\\\(?:\s*\[[^\]]*\])?", body) + rows = [] + for row in raw_rows: + cleaned = row.strip() + if not cleaned: + continue + if "&" not in cleaned and not clean_text(cleaned): + continue + rows.append(cleaned) + return rows + + +def split_into_header_and_data(body: str) -> tuple[list[list[str]], list[list[str]], list[str]]: + """Use \\midrule (or \\hline if no midrule) as the boundary.""" + notes: list[str] = [] + if "\\multicolumn" in body: + notes.append("contains \\multicolumn — header alignment is approximate") + if "\\multirow" in body: + notes.append("contains \\multirow — vertical merges are flattened") + + # Find midrule split (prefer first midrule; fallback to first hline after first row) + boundary = None + for marker in (r"\midrule", r"\hline"): + idx = body.find(marker) + if idx != -1: + boundary = idx + break + + def parse_rows(segment: str) -> list[list[str]]: + rows = [] + for row in split_rows(segment): + cells = [] + for cell in split_top_level_cells(row): + text, span = expand_multicolumn(cell) + cells.append(text) + for _ in range(span - 1): + cells.append("") + rows.append(cells) + return rows + + if boundary is None: + all_rows = parse_rows(body) + header_rows = all_rows[:1] + data_rows = all_rows[1:] + else: + header_rows = parse_rows(body[:boundary]) + data_rows = parse_rows(body[boundary:]) + return header_rows, data_rows, notes + + +def line_for_offset(text: str, offset: int) -> int: + return text.count("\n", 0, offset) + 1 + + +@dataclass +class TableEntry: + file: str + line: int + environment: str + label: str | None = None + caption: str | None = None + column_spec: str | None = None + header_rows: list[list[str]] = field(default_factory=list) + data_rows: list[list[str]] = field(default_factory=list) + raw_rows: int = 0 + notes: list[str] = field(default_factory=list) + + +def find_table_blocks(text: str) -> list[tuple[str, int, int]]: + """Return (env_name, start_offset_of_inner_body, end_offset_of_inner_body).""" + results: list[tuple[str, int, int]] = [] + for env in TABLE_ENVS: + begin = re.compile(r"\\begin\{" + re.escape(env) + r"\}(?:\[[^\]]*\])?", re.S) + end_re = re.compile(r"\\end\{" + re.escape(env) + r"\}") + for match in begin.finditer(text): + inner_start = match.end() + end_match = end_re.search(text, inner_start) + if not end_match: + continue + results.append((env, inner_start, end_match.start())) + return results + + +def extract_from_source(src: Source, root_dir: Path) -> list[TableEntry]: + entries: list[TableEntry] = [] + for env, start, end in find_table_blocks(src.text): + block = src.text[start:end] + caption_raw = find_braced_arg(block, "caption") + label_raw = find_braced_arg(block, "label") + tabular = ROW_TABULAR_ENV_RE.search(block) + + if tabular: + column_spec = tabular.group(2).strip() + body = tabular.group(3) + header_rows, data_rows, notes = split_into_header_and_data(body) + raw_rows = len(split_rows(body)) + else: + column_spec = None + header_rows, data_rows, notes = [], [], ["no tabular/longtable body found"] + raw_rows = 0 + + rel = src.path.relative_to(root_dir) if root_dir in src.path.parents or src.path == root_dir / src.path.name else src.path + entries.append( + TableEntry( + file=str(rel), + line=line_for_offset(src.text, start), + environment=env, + label=clean_text(label_raw) if label_raw else None, + caption=clean_text(caption_raw) if caption_raw else None, + column_spec=column_spec, + header_rows=header_rows, + data_rows=data_rows, + raw_rows=raw_rows, + notes=notes, + ) + ) + return entries + + +def main() -> int: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("paper_tex", type=Path, help="Root .tex file (\\input/\\include is expanded).") + parser.add_argument( + "--max-rows", + type=int, + default=200, + help="Truncate header_rows+data_rows to this many entries per table (default 200).", + ) + parser.add_argument( + "--format", + choices=("json", "summary"), + default="json", + help="json: full ledger; summary: human-readable list with row/col counts.", + ) + args = parser.parse_args() + + if not args.paper_tex.is_file(): + print(f"error: not a file: {args.paper_tex}", file=sys.stderr) + return 2 + + sources = read_with_inputs(args.paper_tex) + if not sources: + print("error: nothing to scan", file=sys.stderr) + return 2 + + root_dir = args.paper_tex.resolve().parent + all_entries: list[TableEntry] = [] + for src in sources: + all_entries.extend(extract_from_source(src, root_dir)) + + if args.format == "summary": + if not all_entries: + print("(no tables detected)") + return 0 + for index, entry in enumerate(all_entries, 1): + cols = max((len(r) for r in entry.header_rows + entry.data_rows), default=0) + data_n = len(entry.data_rows) + head_n = len(entry.header_rows) + label = entry.label or "(no label)" + cap = entry.caption or "(no caption)" + print( + f"[{index}] {entry.file}:{entry.line} env={entry.environment} " + f"label={label} cols={cols} header_rows={head_n} data_rows={data_n}" + ) + print(f" caption: {cap[:200]}") + for note in entry.notes: + print(f" note: {note}") + return 0 + + payload = [] + for entry in all_entries: + rows_kept = entry.data_rows[: max(0, args.max_rows - len(entry.header_rows))] + truncated = len(entry.data_rows) > len(rows_kept) + item = { + "file": entry.file, + "line": entry.line, + "environment": entry.environment, + "label": entry.label, + "caption": entry.caption, + "column_spec": entry.column_spec, + "header_rows": entry.header_rows, + "data_rows": rows_kept, + "data_rows_truncated": truncated, + "raw_rows": entry.raw_rows, + "notes": entry.notes, + } + payload.append(item) + json.dump({"tables": payload}, sys.stdout, ensure_ascii=False, indent=2) + sys.stdout.write("\n") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/scripts/scan_paper.py b/scripts/scan_paper.py index b68564a..47dd00e 100755 --- a/scripts/scan_paper.py +++ b/scripts/scan_paper.py @@ -1,12 +1,22 @@ #!/usr/bin/env python3 -"""Extract a compact content inventory from a LaTeX paper.""" +"""Extract a compact content inventory from a LaTeX paper. + +Multi-file projects are supported: `\\input{...}` and `\\include{...}` +references inside the root .tex are read recursively (depth-limited) so +sectioned papers (`main.tex` + `sections/*.tex`) produce a complete +inventory. +""" from __future__ import annotations import argparse import re +import sys from pathlib import Path +INPUT_RE = re.compile(r"\\(?:input|include|subfile)\{([^{}]+)\}") +MAX_INCLUDE_DEPTH = 6 + def strip_comments(text: str) -> str: lines = [] @@ -17,6 +27,45 @@ def strip_comments(text: str) -> str: return "\n".join(lines) +def read_text_with_warning(path: Path) -> str: + try: + return path.read_text(encoding="utf-8") + except UnicodeDecodeError: + print( + f"warning: {path} is not utf-8, decoded as latin-1 (some characters may be replaced)", + file=sys.stderr, + ) + return path.read_text(encoding="latin-1", errors="replace") + + +def read_with_inputs(root: Path) -> str: + """Concatenate root tex with \\input/\\include children (depth-first).""" + visited: set[Path] = set() + chunks: list[str] = [] + + def load(path: Path, depth: int) -> None: + try: + resolved = path.resolve() + except OSError: + return + if resolved in visited or depth > MAX_INCLUDE_DEPTH or not resolved.is_file(): + return + visited.add(resolved) + text = strip_comments(read_text_with_warning(resolved)) + chunks.append(f"%%% file: {resolved}\n{text}") + for match in INPUT_RE.finditer(text): + ref = match.group(1).strip() + if not ref: + continue + candidate = resolved.parent / ref + if candidate.suffix == "": + candidate = candidate.with_suffix(".tex") + load(candidate, depth + 1) + + load(root, 0) + return "\n\n".join(chunks) + + def find_braced(command: str, text: str) -> list[str]: pattern = re.compile(r"\\" + re.escape(command) + r"(?:\[[^\]]*\])?\{", re.S) results = [] @@ -66,7 +115,11 @@ def main() -> int: parser.add_argument("paper_tex", type=Path) args = parser.parse_args() - text = strip_comments(args.paper_tex.read_text(encoding="utf-8", errors="ignore")) + if not args.paper_tex.is_file(): + print(f"error: not a file: {args.paper_tex}", file=sys.stderr) + return 2 + + text = read_with_inputs(args.paper_tex) print("# Paper Inventory\n") diff --git a/scripts/scan_pdf.py b/scripts/scan_pdf.py new file mode 100755 index 0000000..4c00d79 --- /dev/null +++ b/scripts/scan_pdf.py @@ -0,0 +1,303 @@ +#!/usr/bin/env python3 +"""Extract a compact content inventory from a paper PDF. + +Designed for `kind: pdf_with_assets` inputs where no LaTeX source exists. +The output mirrors `scan_paper.py` so the same downstream workflow applies: +title, authors, abstract, section headings, figures, table regions, links. + +Backends, in order: + 1. PyMuPDF (`pip install pymupdf`) — preferred, gives layout + links. + 2. `pdftotext`/`pdfinfo` (poppler) fallback for text and metadata only. + +If neither is available the script prints a clear error and exits 2. +""" + +from __future__ import annotations + +import argparse +import re +import shutil +import subprocess +import sys +from pathlib import Path +from typing import Iterable + +PLACEHOLDER_TITLE_HINTS = ("untitled", "microsoft word", "arxiv template") +SECTION_RE = re.compile( + r"""^\s*( + (?:\d+(?:\.\d+){0,3})\.?\s+[A-Z][^\n]{2,120} # 1. / 1.2 Section Name + | + (?:Abstract|Introduction|Related\s+Work|Background|Method(?:ology)?| + Approach|Experiments?|Results?|Discussion|Conclusion[s]?| + Limitations?|References|Appendix(?:\s+[A-Z])?)\s*$ + )""", + re.VERBOSE, +) +ABSTRACT_HEAD_RE = re.compile(r"^\s*Abstract\b[:.\s]*", re.IGNORECASE) +NEXT_HEAD_RE = re.compile( + r"^(?:1\b|1\.\s+|Introduction|Keywords|Index Terms|CCS Concepts)\b", + re.IGNORECASE | re.MULTILINE, +) +URL_RE = re.compile(r"https?://[^\s)>\]]+") +EMAIL_RE = re.compile(r"[\w.+-]+@[\w-]+\.[\w.-]+") +WHITESPACE_RE = re.compile(r"\s+") + + +def truncate(value: str, limit: int) -> str: + value = WHITESPACE_RE.sub(" ", value).strip() + if len(value) <= limit: + return value + return value[: limit - 1].rstrip() + "…" + + +def have_pymupdf() -> bool: + try: + import fitz # noqa: F401 + except Exception: + return False + return True + + +def first_page_text_pymupdf(pdf: Path) -> tuple[str, list[str], dict[str, str]]: + import fitz + + with fitz.open(pdf) as doc: + meta = {k: v for k, v in (doc.metadata or {}).items() if v} + first = doc[0].get_text("text") if doc.page_count else "" + all_text = "\n\n".join(page.get_text("text") for page in doc) + return first, [all_text], meta + + +def first_page_text_poppler(pdf: Path) -> tuple[str, list[str], dict[str, str]]: + if not shutil.which("pdftotext"): + raise RuntimeError("Neither pymupdf nor pdftotext is available.") + first = subprocess.run( + ["pdftotext", "-layout", "-f", "1", "-l", "1", str(pdf), "-"], + check=True, + capture_output=True, + text=True, + ).stdout + full = subprocess.run( + ["pdftotext", "-layout", str(pdf), "-"], + check=True, + capture_output=True, + text=True, + ).stdout + meta: dict[str, str] = {} + if shutil.which("pdfinfo"): + info = subprocess.run( + ["pdfinfo", str(pdf)], check=True, capture_output=True, text=True + ).stdout + for line in info.splitlines(): + if ":" in line: + key, _, value = line.partition(":") + meta[key.strip().lower()] = value.strip() + return first, [full], meta + + +def extract_title(first_page: str, meta: dict[str, str]) -> str: + meta_title = (meta.get("title") or meta.get("Title") or "").strip() + if meta_title and not any(h in meta_title.lower() for h in PLACEHOLDER_TITLE_HINTS): + return truncate(meta_title, 240) + + lines = [line.rstrip() for line in first_page.splitlines() if line.strip()] + if not lines: + return "" + # Take consecutive bold-ish-looking lines at the top: skip obvious arxiv/header noise. + skip_prefixes = ("arxiv:", "preprint", "draft", "under review", "to appear") + cleaned: list[str] = [] + for line in lines[:10]: + low = line.strip().lower() + if any(low.startswith(p) for p in skip_prefixes): + continue + if URL_RE.search(line) or EMAIL_RE.search(line): + continue + cleaned.append(line.strip()) + if len(cleaned) >= 3: + break + return truncate(" ".join(cleaned), 240) if cleaned else truncate(lines[0], 240) + + +def extract_authors(first_page: str) -> list[str]: + """Heuristic: lines after the title that look like 'Name, Name, Name'.""" + lines = [line.strip() for line in first_page.splitlines() if line.strip()] + candidates: list[str] = [] + seen_title = False + for line in lines[:25]: + if not seen_title: + seen_title = True + continue + if ABSTRACT_HEAD_RE.match(line): + break + # Author rows tend to contain commas or 'and' and at least one capitalised pair. + if EMAIL_RE.search(line): + continue + if "," in line or " and " in line.lower(): + tokens = re.split(r",|\band\b", line, flags=re.IGNORECASE) + for tok in tokens: + name = tok.strip(" *0123456789†‡§¶") + if not name or len(name) > 80: + continue + if name.lower() in {"abstract", "introduction"}: + continue + if not re.search(r"[A-Za-z]", name): + continue + # A name should have at least two letter-runs. + if len(re.findall(r"[A-Za-zÀ-ɏ][\w'À-ɏ.-]+", name)) >= 2: + candidates.append(name) + if len(candidates) >= 12: + break + # de-dup, keep order + deduped: list[str] = [] + for c in candidates: + if c not in deduped: + deduped.append(c) + return deduped + + +def extract_abstract(full_text: str) -> str: + head = ABSTRACT_HEAD_RE.search(full_text) + if not head: + return "" + tail = full_text[head.end():] + stop = NEXT_HEAD_RE.search(tail) + body = tail[: stop.start()] if stop else tail[:3000] + return truncate(body, 1800) + + +def extract_sections(full_text: str) -> list[str]: + seen: list[str] = [] + for line in full_text.splitlines(): + if SECTION_RE.match(line): + cleaned = WHITESPACE_RE.sub(" ", line).strip(" .") + if cleaned and cleaned not in seen: + seen.append(cleaned) + return seen + + +def extract_links(full_text: str) -> list[str]: + urls: list[str] = [] + for url in URL_RE.findall(full_text): + url = url.rstrip(".,);]>") + if url not in urls: + urls.append(url) + return urls + + +def extract_figure_table_captions(full_text: str) -> tuple[list[str], list[str]]: + figs: list[str] = [] + tabs: list[str] = [] + cap_re = re.compile(r"^\s*(Figure|Fig\.|Table|Tab\.)\s*([A-Za-z0-9.]+)[:.\s]+(.+)") + for line in full_text.splitlines(): + match = cap_re.match(line) + if not match: + continue + kind = match.group(1).lower() + label = match.group(2).strip(".") + body = truncate(match.group(3), 240) + entry = f"{label}: {body}" + if kind.startswith("fig"): + if entry not in figs: + figs.append(entry) + else: + if entry not in tabs: + tabs.append(entry) + return figs, tabs + + +def extract_image_objects(pdf: Path) -> list[str]: + if not have_pymupdf(): + return [] + import fitz + + summaries: list[str] = [] + with fitz.open(pdf) as doc: + for index, page in enumerate(doc, 1): + for img in page.get_images(full=True): + xref, _, width, height, *_ = img + summaries.append(f"page {index}: xref={xref} {width}x{height}") + return summaries + + +def emit(label: str, items: Iterable[str]) -> None: + items = list(items) + if not items: + return + print(f"{label}:") + for item in items: + print(f"- {item}") + print() + + +def main() -> int: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("pdf", type=Path) + parser.add_argument( + "--include-image-objects", + action="store_true", + help="List embedded raster images detected by PyMuPDF (debug aid).", + ) + args = parser.parse_args() + + if not args.pdf.is_file(): + print(f"error: not a file: {args.pdf}", file=sys.stderr) + return 2 + + if have_pymupdf(): + first_page, full_pages, meta = first_page_text_pymupdf(args.pdf) + backend = "pymupdf" + else: + try: + first_page, full_pages, meta = first_page_text_poppler(args.pdf) + backend = "poppler" + except RuntimeError as exc: + print(f"error: {exc}", file=sys.stderr) + print( + "hint: pip install pymupdf (or install poppler-utils for pdftotext)", + file=sys.stderr, + ) + return 2 + + full_text = "\n".join(full_pages) + + print(f"# PDF Inventory (backend: {backend})\n") + + title = extract_title(first_page, meta) + if title: + print(f"Title: {title}\n") + + authors = extract_authors(first_page) + if authors: + print("Authors:") + for name in authors: + print(f"- {name}") + print() + + abstract = extract_abstract(full_text) + if abstract: + print("Abstract:") + print(abstract) + print() + + emit("Sections", extract_sections(full_text)) + figs, tabs = extract_figure_table_captions(full_text) + emit("Figure captions", figs) + emit("Table captions", tabs) + + emit("Links", extract_links(full_text)) + + if args.include_image_objects: + emit("Embedded images", extract_image_objects(args.pdf)) + + if title == "" and not authors and not abstract: + print( + "warning: no title/authors/abstract recovered. PDF may be scanned; " + "consider running OCR (e.g. `ocrmypdf`) before re-scanning.", + file=sys.stderr, + ) + + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/skill.yaml b/skill.yaml index 51adccb..1ffe33d 100644 --- a/skill.yaml +++ b/skill.yaml @@ -1,5 +1,5 @@ name: paper-webpage-builder -version: 0.4.0 +version: 0.5.0 description: Build polished single-page academic project webpages from paper sources, figures, references, and validation scripts. prompts: skill: SKILL.md From b164dfd8fe3b19cfa481a6bf5abfbb867e946053 Mon Sep 17 00:00:00 2001 From: xxllovemkm Date: Tue, 19 May 2026 01:47:37 +0800 Subject: [PATCH 6/6] quality: add webpage validation toolchain --- SKILL.md | 41 +- assets/single-page-template.html | 94 +++- references/design_principles.md | 52 ++ reports/ci-pr-loop/sit-report.html | 413 +++++++++++++++ reports/ci-pr-loop/sit-report.json | 134 +++++ reports/ci-pr-loop/sit-report.md | 57 ++ reports/ci-pr-loop/sit-summary.md | 49 ++ reports/pr-loop.html | 413 +++++++++++++++ schemas/output.schema.json | 42 +- scripts/capture_screenshots.py | 265 ++++++++++ scripts/check_design_drift.py | 799 +++++++++++++++++++++++++++++ scripts/check_html_sanity.py | 234 +++++++++ scripts/check_webpage_links.py | 451 +++++++++++++++- scripts/inject_metadata.py | 175 +++++++ scripts/reconcile_tables.py | 347 +++++++++++++ scripts/render_template.py | 184 +++++++ scripts/scan_paper.py | 20 +- 17 files changed, 3726 insertions(+), 44 deletions(-) create mode 100644 reports/ci-pr-loop/sit-report.html create mode 100644 reports/ci-pr-loop/sit-report.json create mode 100644 reports/ci-pr-loop/sit-report.md create mode 100644 reports/ci-pr-loop/sit-summary.md create mode 100644 reports/pr-loop.html create mode 100755 scripts/capture_screenshots.py create mode 100755 scripts/check_design_drift.py create mode 100755 scripts/check_html_sanity.py create mode 100755 scripts/inject_metadata.py create mode 100755 scripts/reconcile_tables.py create mode 100755 scripts/render_template.py diff --git a/SKILL.md b/SKILL.md index fa67573..df1f2df 100644 --- a/SKILL.md +++ b/SKILL.md @@ -45,23 +45,31 @@ Two failure modes to actively avoid: 5. Design the page around the paper. - Derive colors, background, spacing, figure framing, and motifs from the paper's key figures and domain. - First list the paper-specific visual cues, then choose the background. A plain surface, soft paper tone, lab-notebook grid, dark canvas, figure-derived gradient, or no visible texture are all valid; none is the default. + - Plan figure layout from the actual converted image ratios in `figures.manifest.json`. Wide pipeline/heatmap/table figures, tall case figures, and compact charts should not be forced into one equal-height grid. + - Avoid fixed-height or fixed-aspect figure cards with `object-fit: contain` for paper figures. Use natural image height (`width: 100%; height: auto`) by default; reserve fixed boxes for icons, logos, thumbnails, or zoom modals. + - Put figures side-by-side only when their aspect ratios are compatible, or when one side is balanced by text/table content instead of another image. If max/min ratio is above about 1.25, stack them or use a main-figure-plus-notes composition. - Keep background transitions coherent across sections. Avoid abrupt dark-to-light jumps unless the entire page system intentionally supports that contrast. - - Read `references/design_principles.md` when deciding visual style or revising design feedback. + - Read `references/design_principles.md` when deciding visual style or revising design feedback. The "Measurable Criteria" section there names the thresholds the design-drift check enforces. 6. Implement the webpage. + - Start from `assets/single-page-template.html`. It includes semantic landmarks (`header`/`nav`/`main`/`footer`), a skip-link, a `prefers-reduced-motion` fallback, a CJK-friendly font stack, and the `{{LANG}} {{TITLE}} {{DESCRIPTION}} {{CANONICAL_URL}} {{OG_IMAGE}} {{JSONLD}}` placeholders. Fill them with `scripts/render_template.py` (or `scripts/inject_metadata.py --inplace` if you only need to refresh the head block of an existing page). + - Set `` to the paper's language (BCP-47: `en`, `zh-CN`, `ja`, `ko`, etc.); the scan scripts do not auto-detect this. For CJK papers keep the bundled font stack and add `lang` attributes around any embedded English titles so browsers pick the right glyphs. + - Generate the head metadata once via `scripts/inject_metadata.py --title ... --description ... --canonical ... --og-image ... --author ...` and pipe its `render-values` JSON into `render_template.py`. Use `--arxiv` / `--doi` when known so the ScholarlyArticle JSON-LD includes a stable identifier. - Produce a self-contained single-page `index.html` unless the repo already has a framework. - Include responsive navigation, resource buttons, figure zoom/modal behavior, and readable tables. - - Include full central tables with horizontal scroll on mobile, sticky headers when useful, grouped rows when needed, and highlights for the proposed method or best values. + - For primary paper figures, do not use equal-ratio card grids unless all figures in that grid share a close aspect ratio. If a layout needs equal rhythm, align captions/text, not the image boxes. + - Use compact/narrow table treatments for two- or three-column tables. Do not stretch low-density tables across the full page just because wide result tables need full-width scroll containers, and do not leave narrow tables isolated with a large blank area to the right; pair them with related notes, metrics, or figures. + - Include full central tables with horizontal scroll on mobile, sticky headers when useful, grouped rows when needed, and highlights for the proposed method or best values. Tag each rendered `` (or its wrapping `
`) with `data-tex-label="