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')