Skip to content

Commit 875b6e8

Browse files
authored
Open the question board from the main website (#1192)
* Publish the Open Question Board through the main website * Update question board submission link for the registry directory
1 parent f11b2a4 commit 875b6e8

10 files changed

Lines changed: 121 additions & 33 deletions

File tree

‎.github/workflows/docs.yml‎

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,20 @@ jobs:
1818
steps:
1919
- uses: actions/checkout@v4
2020

21+
- name: Read Open Question Board source
22+
uses: actions/checkout@v4
23+
with:
24+
repository: GiggleLiu/autoresearch-gadgets
25+
ref: main
26+
path: open-questions-source
27+
token: ${{ secrets.PROJECT_READ_TOKEN }}
28+
persist-credentials: false
29+
30+
- uses: astral-sh/setup-uv@c18668ad3cf93ea998bef934396af7bb5c839dc7 # v10.2.0
31+
32+
- name: Build Open Question Board
33+
run: uv run --project open-questions-source/website --locked python open-questions-source/website/build.py
34+
2135
- uses: dtolnay/rust-toolchain@stable
2236

2337
- uses: actions/setup-node@v4
@@ -68,7 +82,7 @@ jobs:
6882
cp -r target/doc/* book/api/
6983
7084
- name: Build research website
71-
run: python3 scripts/build_website.py
85+
run: python3 scripts/build_website.py --open-questions open-questions-source/website/dist
7286

7387
- name: Verify deployment artifact
7488
run: |
@@ -88,6 +102,7 @@ jobs:
88102
path: './book'
89103

90104
deploy:
105+
if: github.ref == 'refs/heads/main'
91106
permissions:
92107
pages: write
93108
id-token: write

‎book.toml‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,4 +36,4 @@ copy-js = true
3636
# Pages that moved to the website keep their published URLs.
3737
[output.html.redirect]
3838
"/reduction-graph.html" = "graph.html"
39-
"/open-problems.html" = "index.html#open-questions"
39+
"/open-problems.html" = "open-questions/"

‎docs/src/design.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -583,4 +583,4 @@ accept full matrices; evaluation and serialization ignore their lower triangle.
583583

584584
## Contributing
585585

586-
See [Call for Contributions](index.html#open-questions) for the recommended issue-based workflow (no coding required).
586+
See the [Open Question Board](open-questions/) to browse problems and register a problem or solution through a GitHub pull request.

‎docs/website/README.md‎

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -127,7 +127,30 @@ clipboard actions, the mathematical example, legacy docs, and mobile overflow.
127127

128128
## Documentation
129129

130-
Open questions has its own website navigation tab at `index.html#open-questions`.
130+
The homepage and graph's **Open questions** link opens `open-questions/`.
131+
The board is generated from the registry and shared templates in
132+
[`GiggleLiu/autoresearch-gadgets`](https://github.com/GiggleLiu/autoresearch-gadgets),
133+
then included in the same Pages artifact. Legacy `#open-questions` and
134+
`open-problems.html` links redirect to the board.
135+
136+
The deployment workflow reads that private repository's `main` branch with the
137+
existing `PROJECT_READ_TOKEN` secret. That token needs **Contents: read** access
138+
to `GiggleLiu/autoresearch-gadgets`; the default `GITHUB_TOKEN` cannot read another
139+
private repository. Only the generated `website/dist/` is published. Registry
140+
changes appear on the next documentation deployment; use **Run workflow** on
141+
`main` to refresh the board without changing this repository. Feature-branch
142+
runs build and verify the artifact without deploying it.
143+
144+
To include the board in a local preview, clone its source beside this repository,
145+
build it with its committed lockfile, and run the website overlay after mdBook:
146+
147+
```sh
148+
uv run --project ../autoresearch-gadgets/website --locked python ../autoresearch-gadgets/website/build.py
149+
python3 scripts/build_website.py --open-questions ../autoresearch-gadgets/website/dist
150+
```
151+
152+
Include the board when running the browser suite. Without `--open-questions`,
153+
the builder previews the main website and guide only.
131154

132155
The guide has seven pages listed in `docs/src/SUMMARY.md`: an overview, the CLI
133156
(quick start, command reference), agent skills, the Rust library

‎docs/website/assets/site.js‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -491,8 +491,8 @@
491491
hydrateHome();
492492
} else if (parts[0] === "atlas") atlasPage();
493493
else if (parts[0] === "open-questions") {
494-
document.title = `Open questions — ${baseTitle}`;
495-
main.innerHTML = '<div class="wrap page-header"><h1>Open questions</h1><p>To be released.</p></div>';
494+
location.replace("./open-questions/");
495+
return;
496496
}
497497
else if (parts[0] === "problem") problemPage(parts[1], params);
498498
else if (parts[0] === "reduction") rulePage(parts[1], parts[2], params);

‎docs/website/graph.html‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@
6161
<a href="./introduction.html" class="docs-nav"
6262
>Docs <span aria-hidden="true">↗</span></a
6363
>
64-
<a href="./#open-questions"
64+
<a href="./open-questions/"
6565
>Open questions <span aria-hidden="true">↗</span></a
6666
>
6767
</nav>

‎docs/website/index.html‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@
5050
<a href="./introduction.html" class="docs-nav"
5151
>Docs <span aria-hidden="true">↗</span></a
5252
>
53-
<a href="#open-questions" data-nav="open-questions"
53+
<a href="./open-questions/"
5454
>Open questions <span aria-hidden="true">↗</span></a
5555
>
5656
</nav>

‎scripts/build_website.py‎

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -115,7 +115,16 @@ def relocate_link(match):
115115
summary.removeprefix("# Summary\n").lstrip())
116116

117117

118-
def build(output, graph_path, schemas_path):
118+
def publish_open_questions(source, output):
119+
"""Publish the board with its own assets, preserving repository evidence paths."""
120+
registry = (source / "registry.json").read_bytes()
121+
shutil.copytree(source, output)
122+
finalize(output)
123+
# Registry paths refer to proof repositories, not this site's asset release.
124+
(output / "registry.json").write_bytes(registry)
125+
126+
127+
def build(output, graph_path, schemas_path, open_questions=None):
119128
graph = json.loads(graph_path.read_text())
120129
schemas = json.loads(schemas_path.read_text())
121130
nodes, edges = graph["nodes"], graph["edges"]
@@ -127,6 +136,9 @@ def build(output, graph_path, schemas_path):
127136
raise ValueError("The atlas requires nodes and valid edge endpoints")
128137
source = ROOT / "docs/website"
129138
output.mkdir(parents=True, exist_ok=True)
139+
# Replace the board on rebuild before finalizing the main site's assets.
140+
if (output / "open-questions").exists():
141+
shutil.rmtree(output / "open-questions")
130142
shutil.copytree(source / "assets", output / "assets", dirs_exist_ok=True)
131143
shutil.copyfile(ROOT / "node_modules/cytoscape/dist/cytoscape.min.js", output / "assets/cytoscape.min.js")
132144
shutil.copyfile(ROOT / "node_modules/cytoscape/LICENSE", output / "assets/cytoscape.LICENSE")
@@ -234,6 +246,8 @@ def build(output, graph_path, schemas_path):
234246
(output / "assets/graph-data.js").write_text("window.REDUCTIONS = " + json.dumps(
235247
{**summary, "layout": layout}, separators=(",", ":")) + ";\n")
236248
finalize(output)
249+
if open_questions is not None:
250+
publish_open_questions(open_questions, output / "open-questions")
237251
print(f"Built website in {output}: {counts['PROBLEM_COUNT']} families, "
238252
f"{len(nodes)} variants, {len(edges)} directed reductions")
239253

@@ -245,9 +259,11 @@ def build(output, graph_path, schemas_path):
245259
default=ROOT / "docs/src/reductions/reduction_graph.json")
246260
parser.add_argument("--schemas", type=Path,
247261
default=ROOT / "docs/src/reductions/problem_schemas.json")
262+
parser.add_argument("--open-questions", type=Path,
263+
help="Built Open Question Board directory (website/dist)")
248264
args = parser.parse_args()
249265
try:
250-
build(args.output, args.graph, args.schemas)
266+
build(args.output, args.graph, args.schemas, args.open_questions)
251267
except FileNotFoundError as error:
252268
parser.exit(1, f"{error}\nGenerate atlas data with cargo run --example "
253269
"export_graph and cargo run --example export_schemas first.\n")

‎scripts/test_website.py‎

Lines changed: 34 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -59,25 +59,29 @@ def test_main_navigation_returns_home_from_problem(self):
5959
expect(home).to_have_attribute('aria-current', 'page')
6060
expect(self.page.locator('main .hero')).to_be_visible()
6161

62-
def test_open_questions_has_its_own_navigation_tab(self):
62+
def test_home_navigation_opens_question_board(self):
6363
self.visit()
6464
tab = self.page.get_by_role('navigation', name='Main navigation').get_by_role('link', name='Open questions', exact=True)
6565
tab.click()
66-
expect(tab).to_have_attribute('aria-current', 'page')
67-
self.assertEqual(tab.locator('xpath=following-sibling::a').count(), 0)
68-
expect(tab.locator('span')).to_have_text('↗')
69-
expect(self.page.get_by_role('heading', name='Open questions', exact=True)).to_be_visible()
70-
expect(self.page.locator('main')).to_contain_text('To be released.')
71-
self.page.goto(self.base + 'introduction.html')
72-
expect(self.page.locator('.sidebar')).not_to_contain_text('Open questions')
73-
expect(self.page.locator('.sidebar').get_by_text('Research', exact=True)).to_have_count(0)
66+
expect(self.page).to_have_url(self.base + 'open-questions/')
67+
expect(self.page.get_by_role('heading', name='Open Question Zoo', exact=True)).to_be_visible()
68+
self.page.get_by_role('link', name='All questions', exact=False).click()
69+
self.page.get_by_role('searchbox', name='Search the collection').fill('Q001')
70+
expect(self.page.locator('.question-item:visible')).to_have_count(1)
71+
expect(self.page.locator('.question-item:visible .question-id')).to_have_text('Q001')
72+
self.page.locator('.question-item:visible .question-link').click()
73+
self.page.get_by_role('link', name='Submit a solution', exact=True).click()
74+
expect(self.page.get_by_label('Registry entry', exact=True)).to_have_value('Q001')
75+
registry = self.page.request.get(self.base + 'open-questions/registry.json').json()
76+
question = next(entry for entry in registry['questions'] if entry['id'] == 'Q001')
77+
expect(self.page.get_by_role('link', name='Edit registry entry on GitHub')).to_have_attribute(
78+
'href', f"https://github.com/GiggleLiu/autoresearch-gadgets/edit/main/registry/{question['slug']}.json")
79+
80+
def test_graph_navigation_opens_question_board(self):
7481
self.page.goto(self.base + 'graph.html')
75-
expect(self.page.locator("#cy canvas").first).to_be_visible()
76-
tab = self.page.get_by_role('link', name='Open questions', exact=True)
77-
self.assertEqual(tab.locator('xpath=following-sibling::a').count(), 0)
78-
expect(tab.locator('span')).to_have_text('↗')
79-
tab.click()
80-
expect(self.page.get_by_role('heading', name='Open questions', exact=True)).to_be_visible()
82+
self.page.get_by_role('link', name='Open questions', exact=True).click()
83+
expect(self.page).to_have_url(self.base + 'open-questions/')
84+
expect(self.page.get_by_role('heading', name='Open Question Zoo', exact=True)).to_be_visible()
8185

8286
def test_home_formulas_load_as_typeset_vectors(self):
8387
self.visit()
@@ -231,13 +235,13 @@ def test_only_turing_rules_have_execution_labels(self):
231235
!link.textContent.includes('Result recovery') && !link.textContent.includes('See contract'))'''))
232236
self.visit('reduction/MaximumIndependentSet/DecisionMaximumIndependentSet')
233237
expect(self.page.locator('.reading-aside .rule-kind')).to_have_text('Turing reduction')
234-
self.visit('reduction/CircuitSAT/SpinGlass')
238+
self.visit('reduction/CircuitSAT/DecisionSpinGlass')
235239
expect(self.page.locator('.reading-aside .rule-kind')).to_have_count(0)
236240
expect(self.page.locator('.reading-aside')).not_to_contain_text('Status')
237241
expect(self.page.locator('.reading-aside')).not_to_contain_text('Capabilities')
238242

239243
def test_rule_parameter_upper_bounds_use_inequalities(self):
240-
self.visit('reduction/CircuitSAT/SpinGlass')
244+
self.visit('reduction/CircuitSAT/DecisionSpinGlass')
241245
expect(self.page.locator('.parameter-operator')).to_have_text(['≤', '≤'])
242246
expect(self.page.locator('.parameter-relation math')).to_have_count(2)
243247
self.page.set_viewport_size({'width': 390, 'height': 900})
@@ -306,8 +310,13 @@ def test_legacy_documentation_urls_redirect(self):
306310
self.page.wait_for_url(self.base + 'graph.html')
307311
expect(self.page.locator("#cy canvas").first).to_be_visible()
308312
self.page.goto(self.base + 'open-problems.html')
309-
self.page.wait_for_url(self.base + 'index.html#open-questions')
310-
expect(self.page.get_by_role('heading', name='Open questions', exact=True)).to_be_visible()
313+
self.page.wait_for_url(self.base + 'open-questions/')
314+
expect(self.page.get_by_role('heading', name='Open Question Zoo', exact=True)).to_be_visible()
315+
316+
def test_legacy_open_questions_hash_opens_question_board(self):
317+
self.visit('open-questions')
318+
expect(self.page).to_have_url(self.base + 'open-questions/')
319+
expect(self.page.get_by_role('heading', name='Open Question Zoo', exact=True)).to_be_visible()
311320

312321
def test_docs_sidebar_drag_resizes_content(self):
313322
self.page.goto(self.base + 'introduction.html')
@@ -403,18 +412,20 @@ def test_graph_is_in_primary_navigation_and_opens_details(self):
403412
""")
404413
self.page.locator('#graph-search').fill('ILP')
405414
self.page.locator('#graph-search').press('Enter')
406-
self.assertEqual(self.page.evaluate("""() => {
415+
graph = self.page.request.get(self.base + 'reductions/reduction_graph.json').json()
416+
expected_variants = sum(node['name'] == 'ILP' for node in graph['nodes'])
417+
self.assertEqual(self.page.evaluate("""expectedVariants => {
407418
const cy = document.querySelector('#cy')._cyreg.cy;
408419
const parent = cy.getElementById('ILP');
409420
const box = parent.boundingBox();
410421
const neighbors = parent.children().connectedEdges().connectedNodes()
411422
.difference(parent.children());
412-
if (parent.children().length !== 4 || neighbors.length <= 50) return ['Missing variants or neighbors'];
423+
if (parent.children().length !== expectedVariants || neighbors.length <= 50) return ['Missing variants or neighbors'];
413424
return neighbors.filter(node => {
414425
const {x, y} = node.position();
415426
return x >= box.x1 && x <= box.x2 && y >= box.y1 && y <= box.y2;
416427
}).map(node => node.id());
417-
}"""), [])
428+
}""", expected_variants), [])
418429
displaced = self.page.evaluate("""() =>
419430
document.querySelector('#cy')._cyreg.cy.nodes()
420431
.filter(node => !node.isParent() && !node.data('isVariant'))
@@ -597,7 +608,7 @@ def test_graph_list_selection_keeps_keyboard_focus(self):
597608
self.assertEqual(self.page.evaluate('document.activeElement.tagName'), 'BODY')
598609

599610
def test_atlas_ilp_prioritizes_documentation_over_metadata(self):
600-
self.visit('problem/ILP')
611+
self.visit('problem/ILP?variant=bounds=general,coefficient=f64,variable=bool')
601612
expect(self.page.locator('article.typst-detail')).to_contain_text('Lenstra')
602613
expect(self.page.locator('.relation-title > span').filter(
603614
has_text='Expected Retrieval Cost')).to_have_text('Expected Retrieval Cost →')

‎scripts/test_website_build.py‎

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,41 @@
11
"""Security and release contracts for the static website build."""
22

33
from pathlib import Path
4+
import json
45
import re
56
import subprocess
67
import tempfile
78
import unittest
89

910
from build_graph_details import SafeDetailHTML
11+
import build_website
1012
from finalize_website import finalize
1113

1214
ROOT = Path(__file__).resolve().parents[1]
1315

1416

1517
class WebsiteBuildTests(unittest.TestCase):
18+
def test_question_board_keeps_assets_and_registry_provenance(self):
19+
with tempfile.TemporaryDirectory() as directory:
20+
root = Path(directory)
21+
source = root / 'board'
22+
(source / 'assets').mkdir(parents=True)
23+
(source / 'assets/site.js').write_text('document.body.dataset.ready = "true";')
24+
(source / 'assets/site.css').write_text('body { color: green; }')
25+
(source / 'index.html').write_text('<html><head><link rel="stylesheet" href="assets/site.css">'
26+
'<script src="assets/site.js" defer></script></head><body>Questions</body></html>')
27+
registry = json.dumps({'questions': [{'id': 'Q001', 'solutions': [
28+
{'materials': [{'path': 'assets/proof.svg'}]}]}]}) + '\n'
29+
(source / 'registry.json').write_text(registry)
30+
output = root / 'site/open-questions'
31+
build_website.publish_open_questions(source, output)
32+
html = (output / 'index.html').read_text()
33+
self.assertIn("script-src 'self'", html)
34+
for url in re.findall(r'(?:src|href)="([^"]+)"', html):
35+
self.assertTrue((output / url.split('?')[0]).is_file(), url)
36+
self.assertEqual((output / 'registry.json').read_text(), registry)
37+
self.assertEqual((source / 'registry.json').read_text(), registry)
38+
1639
def test_documentation_rejects_active_content(self):
1740
for markup in ['<script>alert(1)</script>', '<img onerror="alert(1)">',
1841
'<a href="java&#10;script:alert(1)">bad</a>',

0 commit comments

Comments
 (0)