From 3fe72b705567552f0500cf3f3bc40c16bc7bf57c Mon Sep 17 00:00:00 2001 From: Xiwei Pan <90967972+isPANN@users.noreply.github.com> Date: Sun, 4 Oct 2026 20:26:50 -0700 Subject: [PATCH 1/2] Publish the Open Question Board through the main website --- .github/workflows/docs.yml | 17 ++++++++++- book.toml | 2 +- docs/src/design.md | 2 +- docs/website/README.md | 25 ++++++++++++++- docs/website/assets/site.js | 4 +-- docs/website/graph.html | 2 +- docs/website/index.html | 2 +- scripts/build_website.py | 20 ++++++++++-- scripts/test_website.py | 57 +++++++++++++++++++++-------------- scripts/test_website_build.py | 23 ++++++++++++++ 10 files changed, 121 insertions(+), 33 deletions(-) diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 79ad9258d..bf4b68a7a 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -18,6 +18,20 @@ jobs: steps: - uses: actions/checkout@v4 + - name: Read Open Question Board source + uses: actions/checkout@v4 + with: + repository: GiggleLiu/autoresearch-gadgets + ref: main + path: open-questions-source + token: ${{ secrets.PROJECT_READ_TOKEN }} + persist-credentials: false + + - uses: astral-sh/setup-uv@c18668ad3cf93ea998bef934396af7bb5c839dc7 # v10.2.0 + + - name: Build Open Question Board + run: uv run --project open-questions-source/website --locked python open-questions-source/website/build.py + - uses: dtolnay/rust-toolchain@stable - uses: actions/setup-node@v4 @@ -68,7 +82,7 @@ jobs: cp -r target/doc/* book/api/ - name: Build research website - run: python3 scripts/build_website.py + run: python3 scripts/build_website.py --open-questions open-questions-source/website/dist - name: Verify deployment artifact run: | @@ -88,6 +102,7 @@ jobs: path: './book' deploy: + if: github.ref == 'refs/heads/main' permissions: pages: write id-token: write diff --git a/book.toml b/book.toml index 8ded1c31c..3852b4a86 100644 --- a/book.toml +++ b/book.toml @@ -36,4 +36,4 @@ copy-js = true # Pages that moved to the website keep their published URLs. [output.html.redirect] "/reduction-graph.html" = "graph.html" -"/open-problems.html" = "index.html#open-questions" +"/open-problems.html" = "open-questions/" diff --git a/docs/src/design.md b/docs/src/design.md index 5b58f73ff..1d11d0b6f 100644 --- a/docs/src/design.md +++ b/docs/src/design.md @@ -583,4 +583,4 @@ accept full matrices; evaluation and serialization ignore their lower triangle. ## Contributing -See [Call for Contributions](index.html#open-questions) for the recommended issue-based workflow (no coding required). +See the [Open Question Board](open-questions/) to browse problems and register a problem or solution through a GitHub pull request. diff --git a/docs/website/README.md b/docs/website/README.md index fad5e8574..164cdb213 100644 --- a/docs/website/README.md +++ b/docs/website/README.md @@ -127,7 +127,30 @@ clipboard actions, the mathematical example, legacy docs, and mobile overflow. ## Documentation -Open questions has its own website navigation tab at `index.html#open-questions`. +The homepage and graph's **Open questions** link opens `open-questions/`. +The board is generated from the registry and shared templates in +[`GiggleLiu/autoresearch-gadgets`](https://github.com/GiggleLiu/autoresearch-gadgets), +then included in the same Pages artifact. Legacy `#open-questions` and +`open-problems.html` links redirect to the board. + +The deployment workflow reads that private repository's `main` branch with the +existing `PROJECT_READ_TOKEN` secret. That token needs **Contents: read** access +to `GiggleLiu/autoresearch-gadgets`; the default `GITHUB_TOKEN` cannot read another +private repository. Only the generated `website/dist/` is published. Registry +changes appear on the next documentation deployment; use **Run workflow** on +`main` to refresh the board without changing this repository. Feature-branch +runs build and verify the artifact without deploying it. + +To include the board in a local preview, clone its source beside this repository, +build it with its committed lockfile, and run the website overlay after mdBook: + +```sh +uv run --project ../autoresearch-gadgets/website --locked python ../autoresearch-gadgets/website/build.py +python3 scripts/build_website.py --open-questions ../autoresearch-gadgets/website/dist +``` + +Include the board when running the browser suite. Without `--open-questions`, +the builder previews the main website and guide only. The guide has seven pages listed in `docs/src/SUMMARY.md`: an overview, the CLI (quick start, command reference), agent skills, the Rust library diff --git a/docs/website/assets/site.js b/docs/website/assets/site.js index 15fcb03aa..ab6adb0fd 100644 --- a/docs/website/assets/site.js +++ b/docs/website/assets/site.js @@ -491,8 +491,8 @@ hydrateHome(); } else if (parts[0] === "atlas") atlasPage(); else if (parts[0] === "open-questions") { - document.title = `Open questions — ${baseTitle}`; - main.innerHTML = ''; + location.replace("./open-questions/"); + return; } else if (parts[0] === "problem") problemPage(parts[1], params); else if (parts[0] === "reduction") rulePage(parts[1], parts[2], params); diff --git a/docs/website/graph.html b/docs/website/graph.html index 4581ba890..d01afbbde 100644 --- a/docs/website/graph.html +++ b/docs/website/graph.html @@ -61,7 +61,7 @@ Docs - Open questions diff --git a/docs/website/index.html b/docs/website/index.html index f3a1b318c..6669490a1 100644 --- a/docs/website/index.html +++ b/docs/website/index.html @@ -50,7 +50,7 @@ Docs - Open questions diff --git a/scripts/build_website.py b/scripts/build_website.py index 95f397bac..66ef2326b 100644 --- a/scripts/build_website.py +++ b/scripts/build_website.py @@ -115,7 +115,16 @@ def relocate_link(match): summary.removeprefix("# Summary\n").lstrip()) -def build(output, graph_path, schemas_path): +def publish_open_questions(source, output): + """Publish the board with its own assets, preserving repository evidence paths.""" + registry = (source / "registry.json").read_bytes() + shutil.copytree(source, output) + finalize(output) + # Registry paths refer to proof repositories, not this site's asset release. + (output / "registry.json").write_bytes(registry) + + +def build(output, graph_path, schemas_path, open_questions=None): graph = json.loads(graph_path.read_text()) schemas = json.loads(schemas_path.read_text()) nodes, edges = graph["nodes"], graph["edges"] @@ -127,6 +136,9 @@ def build(output, graph_path, schemas_path): raise ValueError("The atlas requires nodes and valid edge endpoints") source = ROOT / "docs/website" output.mkdir(parents=True, exist_ok=True) + # Replace the board on rebuild before finalizing the main site's assets. + if (output / "open-questions").exists(): + shutil.rmtree(output / "open-questions") shutil.copytree(source / "assets", output / "assets", dirs_exist_ok=True) shutil.copyfile(ROOT / "node_modules/cytoscape/dist/cytoscape.min.js", output / "assets/cytoscape.min.js") shutil.copyfile(ROOT / "node_modules/cytoscape/LICENSE", output / "assets/cytoscape.LICENSE") @@ -234,6 +246,8 @@ def build(output, graph_path, schemas_path): (output / "assets/graph-data.js").write_text("window.REDUCTIONS = " + json.dumps( {**summary, "layout": layout}, separators=(",", ":")) + ";\n") finalize(output) + if open_questions is not None: + publish_open_questions(open_questions, output / "open-questions") print(f"Built website in {output}: {counts['PROBLEM_COUNT']} families, " f"{len(nodes)} variants, {len(edges)} directed reductions") @@ -245,9 +259,11 @@ def build(output, graph_path, schemas_path): default=ROOT / "docs/src/reductions/reduction_graph.json") parser.add_argument("--schemas", type=Path, default=ROOT / "docs/src/reductions/problem_schemas.json") + parser.add_argument("--open-questions", type=Path, + help="Built Open Question Board directory (website/dist)") args = parser.parse_args() try: - build(args.output, args.graph, args.schemas) + build(args.output, args.graph, args.schemas, args.open_questions) except FileNotFoundError as error: parser.exit(1, f"{error}\nGenerate atlas data with cargo run --example " "export_graph and cargo run --example export_schemas first.\n") diff --git a/scripts/test_website.py b/scripts/test_website.py index 5b5ad5b07..0a8bcbc76 100644 --- a/scripts/test_website.py +++ b/scripts/test_website.py @@ -59,25 +59,29 @@ def test_main_navigation_returns_home_from_problem(self): expect(home).to_have_attribute('aria-current', 'page') expect(self.page.locator('main .hero')).to_be_visible() - def test_open_questions_has_its_own_navigation_tab(self): + def test_home_navigation_opens_question_board(self): self.visit() tab = self.page.get_by_role('navigation', name='Main navigation').get_by_role('link', name='Open questions', exact=True) tab.click() - expect(tab).to_have_attribute('aria-current', 'page') - self.assertEqual(tab.locator('xpath=following-sibling::a').count(), 0) - expect(tab.locator('span')).to_have_text('↗') - expect(self.page.get_by_role('heading', name='Open questions', exact=True)).to_be_visible() - expect(self.page.locator('main')).to_contain_text('To be released.') - self.page.goto(self.base + 'introduction.html') - expect(self.page.locator('.sidebar')).not_to_contain_text('Open questions') - expect(self.page.locator('.sidebar').get_by_text('Research', exact=True)).to_have_count(0) + expect(self.page).to_have_url(self.base + 'open-questions/') + expect(self.page.get_by_role('heading', name='Open Question Zoo', exact=True)).to_be_visible() + self.page.get_by_role('link', name='All questions', exact=False).click() + self.page.get_by_role('searchbox', name='Search the collection').fill('Q001') + expect(self.page.locator('.question-item:visible')).to_have_count(1) + expect(self.page.locator('.question-item:visible .question-id')).to_have_text('Q001') + self.page.locator('.question-item:visible .question-link').click() + self.page.get_by_role('link', name='Submit a solution', exact=True).click() + expect(self.page.get_by_label('Registry entry', exact=True)).to_have_value('Q001') + registry = self.page.request.get(self.base + 'open-questions/registry.json').json() + question = next(entry for entry in registry['questions'] if entry['id'] == 'Q001') + expect(self.page.get_by_role('link', name='Edit registry entry on GitHub')).to_have_attribute( + 'href', f"https://github.com/GiggleLiu/autoresearch-gadgets/edit/main/website/questions/{question['slug']}.json") + + def test_graph_navigation_opens_question_board(self): self.page.goto(self.base + 'graph.html') - expect(self.page.locator("#cy canvas").first).to_be_visible() - tab = self.page.get_by_role('link', name='Open questions', exact=True) - self.assertEqual(tab.locator('xpath=following-sibling::a').count(), 0) - expect(tab.locator('span')).to_have_text('↗') - tab.click() - expect(self.page.get_by_role('heading', name='Open questions', exact=True)).to_be_visible() + self.page.get_by_role('link', name='Open questions', exact=True).click() + expect(self.page).to_have_url(self.base + 'open-questions/') + expect(self.page.get_by_role('heading', name='Open Question Zoo', exact=True)).to_be_visible() def test_home_formulas_load_as_typeset_vectors(self): self.visit() @@ -231,13 +235,13 @@ def test_only_turing_rules_have_execution_labels(self): !link.textContent.includes('Result recovery') && !link.textContent.includes('See contract'))''')) self.visit('reduction/MaximumIndependentSet/DecisionMaximumIndependentSet') expect(self.page.locator('.reading-aside .rule-kind')).to_have_text('Turing reduction') - self.visit('reduction/CircuitSAT/SpinGlass') + self.visit('reduction/CircuitSAT/DecisionSpinGlass') expect(self.page.locator('.reading-aside .rule-kind')).to_have_count(0) expect(self.page.locator('.reading-aside')).not_to_contain_text('Status') expect(self.page.locator('.reading-aside')).not_to_contain_text('Capabilities') def test_rule_parameter_upper_bounds_use_inequalities(self): - self.visit('reduction/CircuitSAT/SpinGlass') + self.visit('reduction/CircuitSAT/DecisionSpinGlass') expect(self.page.locator('.parameter-operator')).to_have_text(['≤', '≤']) expect(self.page.locator('.parameter-relation math')).to_have_count(2) self.page.set_viewport_size({'width': 390, 'height': 900}) @@ -306,8 +310,13 @@ def test_legacy_documentation_urls_redirect(self): self.page.wait_for_url(self.base + 'graph.html') expect(self.page.locator("#cy canvas").first).to_be_visible() self.page.goto(self.base + 'open-problems.html') - self.page.wait_for_url(self.base + 'index.html#open-questions') - expect(self.page.get_by_role('heading', name='Open questions', exact=True)).to_be_visible() + self.page.wait_for_url(self.base + 'open-questions/') + expect(self.page.get_by_role('heading', name='Open Question Zoo', exact=True)).to_be_visible() + + def test_legacy_open_questions_hash_opens_question_board(self): + self.visit('open-questions') + expect(self.page).to_have_url(self.base + 'open-questions/') + expect(self.page.get_by_role('heading', name='Open Question Zoo', exact=True)).to_be_visible() def test_docs_sidebar_drag_resizes_content(self): self.page.goto(self.base + 'introduction.html') @@ -403,18 +412,20 @@ def test_graph_is_in_primary_navigation_and_opens_details(self): """) self.page.locator('#graph-search').fill('ILP') self.page.locator('#graph-search').press('Enter') - self.assertEqual(self.page.evaluate("""() => { + graph = self.page.request.get(self.base + 'reductions/reduction_graph.json').json() + expected_variants = sum(node['name'] == 'ILP' for node in graph['nodes']) + self.assertEqual(self.page.evaluate("""expectedVariants => { const cy = document.querySelector('#cy')._cyreg.cy; const parent = cy.getElementById('ILP'); const box = parent.boundingBox(); const neighbors = parent.children().connectedEdges().connectedNodes() .difference(parent.children()); - if (parent.children().length !== 4 || neighbors.length <= 50) return ['Missing variants or neighbors']; + if (parent.children().length !== expectedVariants || neighbors.length <= 50) return ['Missing variants or neighbors']; return neighbors.filter(node => { const {x, y} = node.position(); return x >= box.x1 && x <= box.x2 && y >= box.y1 && y <= box.y2; }).map(node => node.id()); - }"""), []) + }""", expected_variants), []) displaced = self.page.evaluate("""() => document.querySelector('#cy')._cyreg.cy.nodes() .filter(node => !node.isParent() && !node.data('isVariant')) @@ -597,7 +608,7 @@ def test_graph_list_selection_keeps_keyboard_focus(self): self.assertEqual(self.page.evaluate('document.activeElement.tagName'), 'BODY') def test_atlas_ilp_prioritizes_documentation_over_metadata(self): - self.visit('problem/ILP') + self.visit('problem/ILP?variant=bounds=general,coefficient=f64,variable=bool') expect(self.page.locator('article.typst-detail')).to_contain_text('Lenstra') expect(self.page.locator('.relation-title > span').filter( has_text='Expected Retrieval Cost')).to_have_text('Expected Retrieval Cost →') diff --git a/scripts/test_website_build.py b/scripts/test_website_build.py index 8a4d0430c..31141f6cc 100644 --- a/scripts/test_website_build.py +++ b/scripts/test_website_build.py @@ -1,18 +1,41 @@ """Security and release contracts for the static website build.""" from pathlib import Path +import json import re import subprocess import tempfile import unittest from build_graph_details import SafeDetailHTML +import build_website from finalize_website import finalize ROOT = Path(__file__).resolve().parents[1] class WebsiteBuildTests(unittest.TestCase): + def test_question_board_keeps_assets_and_registry_provenance(self): + with tempfile.TemporaryDirectory() as directory: + root = Path(directory) + source = root / 'board' + (source / 'assets').mkdir(parents=True) + (source / 'assets/site.js').write_text('document.body.dataset.ready = "true";') + (source / 'assets/site.css').write_text('body { color: green; }') + (source / 'index.html').write_text('' + 'Questions') + registry = json.dumps({'questions': [{'id': 'Q001', 'solutions': [ + {'materials': [{'path': 'assets/proof.svg'}]}]}]}) + '\n' + (source / 'registry.json').write_text(registry) + output = root / 'site/open-questions' + build_website.publish_open_questions(source, output) + html = (output / 'index.html').read_text() + self.assertIn("script-src 'self'", html) + for url in re.findall(r'(?:src|href)="([^"]+)"', html): + self.assertTrue((output / url.split('?')[0]).is_file(), url) + self.assertEqual((output / 'registry.json').read_text(), registry) + self.assertEqual((source / 'registry.json').read_text(), registry) + def test_documentation_rejects_active_content(self): for markup in ['', '', 'bad', From b8a8d866e2cb7152568be5852f12e3fee24f9f0b Mon Sep 17 00:00:00 2001 From: Xiwei Pan <90967972+isPANN@users.noreply.github.com> Date: Sun, 4 Oct 2026 22:08:35 -0700 Subject: [PATCH 2/2] Update question board submission link for the registry directory --- scripts/test_website.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/test_website.py b/scripts/test_website.py index 0a8bcbc76..cd9d2adf1 100644 --- a/scripts/test_website.py +++ b/scripts/test_website.py @@ -75,7 +75,7 @@ def test_home_navigation_opens_question_board(self): registry = self.page.request.get(self.base + 'open-questions/registry.json').json() question = next(entry for entry in registry['questions'] if entry['id'] == 'Q001') expect(self.page.get_by_role('link', name='Edit registry entry on GitHub')).to_have_attribute( - 'href', f"https://github.com/GiggleLiu/autoresearch-gadgets/edit/main/website/questions/{question['slug']}.json") + 'href', f"https://github.com/GiggleLiu/autoresearch-gadgets/edit/main/registry/{question['slug']}.json") def test_graph_navigation_opens_question_board(self): self.page.goto(self.base + 'graph.html')