diff --git a/.docs/README.md b/.docs/README.md index 9853715..f175546 100644 --- a/.docs/README.md +++ b/.docs/README.md @@ -1,14 +1,23 @@ -# Research notes +# Documentation -Working notes for the learning-for-planning line in `jupyddl`. These are -research documents, not user documentation — the user-facing description of -`jupyddl learn` is in the README and the module docstrings. +Everything that is not the README lives here: the research notes for the +learning-for-planning line, the release runbook, and every image, video and +measurement cache under [`assets/`](assets). + +These notes are research documents, not user documentation — the user-facing +description of `jupyddl learn` is in the README and the module docstrings. + +The two files GitHub reads for its own community features, +`CONTRIBUTING.md` and `CODE_OF_CONDUCT.md`, are in `.github/` instead: GitHub +only looks for them in the repository root, `.github/` or `docs/`, and would +not find them here. | Note | What it covers | |---|---| | [learned-heuristics.md](learned-heuristics.md) | The imitation stage: prior work, the design we chose, measured results on three domains, and an analysis of the one domain where it loses badly | | [rl-for-search.md](rl-for-search.md) | The reinforcement stage: the MDP that "minimise expansions" corresponds to, why the obvious policy gradient is hard here, and what we do instead | | [roadmap.md](roadmap.md) | What to build next, ordered by expected value, with the experiment that would settle each | +| [RELEASING.md](RELEASING.md) | How a release is cut, and the one-time PyPI trusted-publisher setup only a maintainer can do | ## The one-paragraph version @@ -36,16 +45,16 @@ edited out. ## The video -[`promo/jupyddl-rl.mp4`](../promo/jupyddl-rl.mp4) is a 97-second tour of this +[`assets/jupyddl-rl.mp4`](assets/jupyddl-rl.mp4) is a 97-second tour of this work. Like the main promo it measures everything at render time — it trains, reinforces, and re-runs both failure modes — so it cannot drift from these notes. Rebuild it with: ```bash -python tools/make_learn_promo.py --cache promo/rl-data.json -o promo/jupyddl-rl.mp4 +python tools/make_learn_promo.py --cache .docs/assets/rl-data.json -o .docs/assets/jupyddl-rl.mp4 ``` -`promo/rl-data.json` is the cached measurement pass; delete it to re-measure. +`assets/rl-data.json` is the cached measurement pass; delete it to re-measure. ## Reproducing everything here diff --git a/docs/RELEASING.md b/.docs/RELEASING.md similarity index 100% rename from docs/RELEASING.md rename to .docs/RELEASING.md diff --git a/docs/images/benchmark-dark.png b/.docs/assets/benchmark-dark.png similarity index 100% rename from docs/images/benchmark-dark.png rename to .docs/assets/benchmark-dark.png diff --git a/docs/images/benchmark.png b/.docs/assets/benchmark.png similarity index 100% rename from docs/images/benchmark.png rename to .docs/assets/benchmark.png diff --git a/docs/images/comparison-dark.png b/.docs/assets/comparison-dark.png similarity index 100% rename from docs/images/comparison-dark.png rename to .docs/assets/comparison-dark.png diff --git a/docs/images/comparison.png b/.docs/assets/comparison.png similarity index 100% rename from docs/images/comparison.png rename to .docs/assets/comparison.png diff --git a/promo/jupyddl-promo.mp4 b/.docs/assets/jupyddl-promo.mp4 similarity index 100% rename from promo/jupyddl-promo.mp4 rename to .docs/assets/jupyddl-promo.mp4 diff --git a/promo/jupyddl-rl.mp4 b/.docs/assets/jupyddl-rl.mp4 similarity index 100% rename from promo/jupyddl-rl.mp4 rename to .docs/assets/jupyddl-rl.mp4 diff --git a/promo/rl-data.json b/.docs/assets/rl-data.json similarity index 100% rename from promo/rl-data.json rename to .docs/assets/rl-data.json diff --git a/docs/images/search-progress-dark.png b/.docs/assets/search-progress-dark.png similarity index 100% rename from docs/images/search-progress-dark.png rename to .docs/assets/search-progress-dark.png diff --git a/docs/images/search-progress.png b/.docs/assets/search-progress.png similarity index 100% rename from docs/images/search-progress.png rename to .docs/assets/search-progress.png diff --git a/docs/images/wavefront-dark.png b/.docs/assets/wavefront-dark.png similarity index 100% rename from docs/images/wavefront-dark.png rename to .docs/assets/wavefront-dark.png diff --git a/docs/images/wavefront.png b/.docs/assets/wavefront.png similarity index 100% rename from docs/images/wavefront.png rename to .docs/assets/wavefront.png diff --git a/promo/workbench-dark.png b/.docs/assets/workbench-dark.png similarity index 100% rename from promo/workbench-dark.png rename to .docs/assets/workbench-dark.png diff --git a/promo/workbench-experiment.png b/.docs/assets/workbench-experiment.png similarity index 100% rename from promo/workbench-experiment.png rename to .docs/assets/workbench-experiment.png diff --git a/promo/workbench-requirements.png b/.docs/assets/workbench-requirements.png similarity index 100% rename from promo/workbench-requirements.png rename to .docs/assets/workbench-requirements.png diff --git a/promo/workbench-solve.png b/.docs/assets/workbench-solve.png similarity index 100% rename from promo/workbench-solve.png rename to .docs/assets/workbench-solve.png diff --git a/docs/CODE_OF_CONDUCT.md b/.github/CODE_OF_CONDUCT.md similarity index 100% rename from docs/CODE_OF_CONDUCT.md rename to .github/CODE_OF_CONDUCT.md diff --git a/docs/CONTRIBUTING.md b/.github/CONTRIBUTING.md similarity index 100% rename from docs/CONTRIBUTING.md rename to .github/CONTRIBUTING.md diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 687e850..abae49b 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -8,7 +8,7 @@ name: release # # Authentication is PyPI **Trusted Publishing** (OIDC), so there is no API # token in this repository's secrets to leak or rotate. That requires a -# one-time setup on PyPI — see `docs/RELEASING.md`. Until it is done the +# one-time setup on PyPI — see `.docs/RELEASING.md`. Until it is done the # `pypi` job fails and everything before it still succeeds, so a tag never # leaves you with half a release and no artifacts. # diff --git a/.gitignore b/.gitignore index ed762ac..1a0a544 100644 --- a/.gitignore +++ b/.gitignore @@ -152,4 +152,4 @@ web/vendor/ # Generated galleries and animations gallery/ *.mp4 -!promo/*.mp4 +!.docs/assets/*.mp4 diff --git a/AGENTS.md b/AGENTS.md index 6d54787..d94f625 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -46,7 +46,7 @@ native build step, and the core has zero runtime dependencies. with `pyproject.toml` or a `__version__` that disagrees with either, runs `twine check --strict`, installs the wheel clean and plans with it, publishes to PyPI over OIDC (no stored token), then cuts the GitHub Release from the - changelog section for that version. `docs/RELEASING.md` is the runbook, + changelog section for that version. `.docs/RELEASING.md` is the runbook, including the one-time PyPI trusted-publisher setup only a maintainer can do. **Bump the version in two places** — `pyproject.toml` and `jupyddl/__init__.py` — and rebuild `web/dist`, which carries it too. @@ -77,7 +77,7 @@ native build step, and the core has zero runtime dependencies. - `web/` — the Pyodide playground; `tools/build_web.py` bundles the package sources and demos into `web/dist` (committed). It also writes `capabilities.json` (the registries) and `research.json` (distilled from - `promo/rl-data.json`, so the page and the RL video quote one measured run). + `.docs/assets/rl-data.json`, so the page and the RL video quote one measured run). Those two are rendered **before** Pyodide loads — the app shell is never hidden, and only the run controls are gated on `state.ready` — so a stale bundle briefly states something untrue rather than merely lagging. @@ -85,7 +85,18 @@ native build step, and the core has zero runtime dependencies. - `tools/make_promo.py` — renders the main promo video from measured runs. - `tools/make_learn_promo.py` — the learned-heuristic/RL video. It re-measures everything including both failure modes, so it cannot drift from `.docs/`; - `promo/rl-data.json` caches the pass, delete it to re-measure. + `.docs/assets/rl-data.json` caches the pass, delete it to re-measure. +- **`.docs/` is the only documentation directory.** Research notes and the + release runbook sit at its top level; every image, video and measurement + cache goes in `.docs/assets/`. There is no `docs/` or `promo/` — they were + merged in because three directories with no rule between them meant every + new file was a guess. The two exceptions live in `.github/`: + `CONTRIBUTING.md` and `CODE_OF_CONDUCT.md`, which GitHub only recognises in + the root, `.github/` or `docs/`. + The sdist excludes `.docs/assets/*.png` and `*.mp4` **by extension, not by + directory**, because `rl-data.json` sits beside them and + `tests/test_web_bundle.py` reads it — excluding the directory ships an sdist + whose own suite fails. ### The condition pipeline Conditions are a **formula tree in negation normal form**: `parse_condition` diff --git a/CHANGELOG.md b/CHANGELOG.md index 6a824b4..c58ca64 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,24 @@ project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [Unreleased] +### Changed +- **Documentation lives in one place: `.docs/`.** It was spread over three + directories — `docs/` for the contributor files and the README charts, + `promo/` for the videos and screenshots, `.docs/` for the research notes — + with no rule saying which took what, so every new file was a guess. The + research notes, the release runbook and every image, video and measurement + cache are now under `.docs/`, with binaries in `.docs/assets/`. This also + matches `pymapf`, the sibling project. + + Paths quoted in the released sections below are the ones that were correct + at the time. `promo/rl-data.json` is now `.docs/assets/rl-data.json`, and + `docs/images/` is now `.docs/assets/`. + + `CONTRIBUTING.md` and `CODE_OF_CONDUCT.md` moved to `.github/` rather than + `.docs/`: GitHub only recognises those two files in the repository root, + `.github/` or `docs/`, so filing them under `.docs/` would have silently + dropped the contributing link on the issue and pull-request forms. + ## [2.4.0] ### Added @@ -15,8 +33,8 @@ project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). measured run: imitation against `hff` and `goalcount`, the per-instance spread behind the mean, the logistics loss and why the feature space causes it, and the three claims that turned out to be wrong. Its numbers are built - from `promo/rl-data.json` — the same cache the RL video renders from — so the - page and the video cannot drift apart, and a test pins them together. + from `promo/rl-data.json` — the same cache the RL video renders from — so + the page and the video cannot drift apart, and a test pins them together. ### Fixed - **Reading the workbench no longer costs a 10 MB download.** `
` was diff --git a/README.md b/README.md index 1ae3021..4d61dcf 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@ classical-to-SOTA planners, heuristics, benchmarking — and a search you can actually *watch*. **[▶ Open the workbench](https://openplan-labs.github.io/PythonPDDL/)** · -[Watch the 105-second tour](promo/jupyddl-promo.mp4) +[Watch the 105-second tour](.docs/assets/jupyddl-promo.mp4) The repository is `PythonPDDL`; the package is `jupyddl`. That is what you install and what you import. @@ -33,8 +33,8 @@ is trivial to install, embed, teach with, and build on.
- - Four planners compared on an 8-block blocksworld instance: nodes expanded on a log scale, runtime, plan cost, and the heuristic estimate along the search. + + Four planners compared on an 8-block blocksworld instance: nodes expanded on a log scale, runtime, plan cost, and the heuristic estimate along the search.
@@ -203,8 +203,8 @@ returns exactly the same plan and statistics as an unobserved one.)
- - Four panels showing an A*/LM-cut search on the gripper instance: f/g/h cost estimates, frontier size, expansion depth, and cumulative nodes over time. + + Four panels showing an A*/LM-cut search on the gripper instance: f/g/h cost estimates, frontier size, expansion depth, and cumulative nodes over time.
@@ -216,8 +216,8 @@ blind one fills the whole disc:
- - Radial chart of a breadth-first search: thousands of expanded nodes arranged in concentric rings by depth, coloured by heuristic value. + + Radial chart of a breadth-first search: thousands of expanded nodes arranged in concentric rings by depth, coloured by heuristic value.
@@ -281,7 +281,7 @@ never costs a 10 MB download.
-The jupyddl workbench: a PDDL editor beside live cost-estimate charts, a radial search wavefront, and the resulting validated plan. +The jupyddl workbench: a PDDL editor beside live cost-estimate charts, a radial search wavefront, and the resulting validated plan.
@@ -350,7 +350,7 @@ seen, against greedy best-first search: Nearly four times fewer expansions than `hff` and about fifteen times faster, because the network is a thousand multiply-adds and `hff` is a relaxed-plan -extraction. These are read from `promo/rl-data.json`, the cache the published +extraction. These are read from `.docs/assets/rl-data.json`, the cache the published [Research view](https://openplan-labs.github.io/PythonPDDL/) renders from, so the page and this table cannot drift apart. @@ -359,7 +359,7 @@ exact rather than mysterious: that domain has two predicates, so the feature vector cannot tell *which* package is where, only how many are somewhere. That result, the RL formulation, and what to build next are written up in [`.docs/`](.docs/), and there is a -[97-second tour of the RL half](promo/jupyddl-rl.mp4) — including the two +[97-second tour of the RL half](.docs/assets/jupyddl-rl.mp4) — including the two measurement mistakes that shaped the design. Read the 137 as a mean over a heavy tail: nine of the ten held-out instances sit @@ -420,8 +420,8 @@ jupyddl benchmark demos --planners astar,gbfs,ehc,bfs --heuristic hff \
- - Benchmark dashboard: coverage, total nodes expanded on a log scale, total runtime, and a per-instance heatmap of nodes expanded across six demo instances and five planner configurations. + + Benchmark dashboard: coverage, total nodes expanded on a log scale, total runtime, and a per-instance heatmap of nodes expanded across six demo instances and five planner configurations.
@@ -588,8 +588,8 @@ Regenerating the media: ```bash python tools/build_web.py # playground bundle -python tools/make_promo.py -o promo/jupyddl-promo.mp4 \ - --screenshot promo/workbench-dark.png # the promo video +python tools/make_promo.py -o .docs/assets/jupyddl-promo.mp4 \ + --screenshot .docs/assets/workbench-dark.png # the promo video ``` Every number in the promo video is measured at render time by running the real diff --git a/pyproject.toml b/pyproject.toml index 9207776..5a0776d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -58,13 +58,16 @@ packages = ["jupyddl"] # An sdist should carry what is needed to build and test the package, not the # project's marketing. Left to its defaults hatchling packed the promo video # and every screenshot and chart, making a 6.6 MB sdist against a 105 KB wheel. -# `web/` stays: `tests/test_web_bundle.py` reads it, so dropping it would leave -# an sdist whose own test suite fails. +# +# The media under `.docs/assets/` is excluded by extension rather than by +# excluding the directory, because `rl-data.json` lives beside it and +# `tests/test_web_bundle.py` reads it — as does `web/`, which stays for the +# same reason. Dropping either leaves an sdist whose own test suite fails. [tool.hatch.build.targets.sdist] exclude = [ ".github/**", - "docs/images/**", - "promo/**", + ".docs/assets/*.png", + ".docs/assets/*.mp4", "logs/**", ] diff --git a/tests/test_web_bundle.py b/tests/test_web_bundle.py index fd79639..e94aed1 100644 --- a/tests/test_web_bundle.py +++ b/tests/test_web_bundle.py @@ -119,7 +119,7 @@ def test_capabilities_bundle_agrees_with_the_registries(): def test_research_bundle_quotes_the_measured_run(): """The Research view must not invent numbers. - ``collect_research`` distils ``promo/rl-data.json`` — the cache the RL + ``collect_research`` distils ``.docs/assets/rl-data.json`` — the cache the RL video renders from — so page and video quote one measured run and cannot drift apart. When that file is absent the builder emits ``{}`` and the view says so; that is the only other acceptable state. @@ -130,7 +130,7 @@ def test_research_bundle_quotes_the_measured_run(): with open(path, encoding="utf-8") as handle: research = json.load(handle) - measured = os.path.join(REPO_ROOT, "promo", "rl-data.json") + measured = os.path.join(REPO_ROOT, ".docs", "assets", "rl-data.json") if not research: assert not os.path.exists(measured) return @@ -210,3 +210,46 @@ def test_bootstrap_is_valid_python(): path = os.path.join(WEB, "bootstrap.py") with open(path, encoding="utf-8") as handle: compile(handle.read(), path, "exec") + + +def test_documentation_lives_in_one_directory(): + """`.docs/` is the only documentation directory. + + It used to be three — `docs/` for contributor files and README charts, + `promo/` for videos, `.docs/` for research notes — with no rule saying + which took what, so every new file was a guess. The two community files + are the deliberate exception: GitHub only recognises `CONTRIBUTING.md` + and `CODE_OF_CONDUCT.md` in the root, `.github/` or `docs/`, so filing + them under `.docs/` would drop the contributing link on the issue and + pull-request forms. + """ + for gone in ("docs", "promo"): + assert not os.path.isdir( + os.path.join(REPO_ROOT, gone) + ), f"{gone}/ is back; documentation belongs in .docs/" + + for name in ("README.md", "RELEASING.md", "learned-heuristics.md"): + assert os.path.exists(os.path.join(REPO_ROOT, ".docs", name)) + + for name in ("CONTRIBUTING.md", "CODE_OF_CONDUCT.md"): + assert os.path.exists( + os.path.join(REPO_ROOT, ".github", name) + ), f"{name} must stay somewhere GitHub looks for it" + + +def test_the_sdist_keeps_the_data_its_tests_read(): + """The media is excluded by extension, not by excluding `.docs/assets/`. + + `rl-data.json` sits beside the images and videos, and + ``test_research_bundle_quotes_the_measured_run`` opens it. Excluding the + directory outright would ship an sdist whose own suite fails on a missing + file — which is exactly how the 6.6 MB sdist fix broke two parser tests. + """ + with open(os.path.join(REPO_ROOT, "pyproject.toml"), encoding="utf-8") as handle: + pyproject = handle.read() + assert '".docs/assets/*.png"' in pyproject + assert '".docs/assets/*.mp4"' in pyproject + assert '".docs/assets/**"' not in pyproject, ( + "excluding the whole assets directory drops rl-data.json, " + "which the test suite reads" + ) diff --git a/tools/build_web.py b/tools/build_web.py index 9398279..9b7bffe 100644 --- a/tools/build_web.py +++ b/tools/build_web.py @@ -204,12 +204,12 @@ def collect_capabilities() -> dict: def collect_research() -> dict: """Distil the learned-heuristic measurements for the Research view. - Read from ``promo/rl-data.json`` — the cache the RL promo video renders + Read from ``.docs/assets/rl-data.json`` — the cache the RL promo video renders from — so the page and the video quote the same measured run and cannot drift apart. Returns ``{}`` when that file is absent, and the view then says so rather than showing numbers from nowhere. """ - path = os.path.join(ROOT, "promo", "rl-data.json") + path = os.path.join(ROOT, ".docs", "assets", "rl-data.json") if not os.path.exists(path): return {} with open(path, encoding="utf-8") as handle: @@ -307,7 +307,7 @@ def main() -> int: print( " learned-heuristic measurements -> web/dist/research.json" if research - else " ! no promo/rl-data.json; the Research view will say so" + else " ! no .docs/assets/rl-data.json; the Research view will say so" ) return 0 diff --git a/tools/make_learn_promo.py b/tools/make_learn_promo.py index 0499220..e07e873 100644 --- a/tools/make_learn_promo.py +++ b/tools/make_learn_promo.py @@ -6,12 +6,12 @@ that shaped its design, and animates whatever came back. If the method gets better or worse, so does the video. - python tools/make_learn_promo.py -o promo/jupyddl-rl.mp4 + python tools/make_learn_promo.py -o .docs/assets/jupyddl-rl.mp4 Collection takes a few minutes, so it is cached:: - python tools/make_learn_promo.py --cache promo/rl-data.json # measure once - python tools/make_learn_promo.py --cache promo/rl-data.json # reuse + python tools/make_learn_promo.py --cache .docs/assets/rl-data.json # measure once + python tools/make_learn_promo.py --cache .docs/assets/rl-data.json # reuse Needs the ``viz`` extra plus an ffmpeg binary; ``learn`` (NumPy) makes the collection pass much faster but is not required. @@ -1329,7 +1329,7 @@ def main() -> int: parser = argparse.ArgumentParser( description="Render the learned-heuristic / RL promo video." ) - parser.add_argument("-o", "--output", default="promo/jupyddl-rl.mp4") + parser.add_argument("-o", "--output", default=".docs/assets/jupyddl-rl.mp4") parser.add_argument("--fps", type=int, default=FPS) parser.add_argument("--dpi", type=int, default=DPI) parser.add_argument( diff --git a/tools/make_promo.py b/tools/make_promo.py index 23c2203..9f3564b 100644 --- a/tools/make_promo.py +++ b/tools/make_promo.py @@ -5,7 +5,7 @@ real planners on the bundled demo instances first, then animates what came back. If a search gets faster or slower, the video changes with it. - python tools/make_promo.py -o promo/jupyddl.mp4 + python tools/make_promo.py -o .docs/assets/jupyddl.mp4 Needs the ``viz`` extra plus an ffmpeg binary (``pip install imageio-ffmpeg`` is enough — matplotlib is pointed at the bundled static build automatically). @@ -1677,7 +1677,7 @@ def render(data, out: str, screenshot=None, fps: int = FPS, dpi: int = DPI): def main() -> int: parser = argparse.ArgumentParser(description="Render the jupyddl promo video.") - parser.add_argument("-o", "--output", default="promo/jupyddl.mp4") + parser.add_argument("-o", "--output", default=".docs/assets/jupyddl.mp4") parser.add_argument("--demos", default="demos") parser.add_argument("--fps", type=int, default=FPS) parser.add_argument("--dpi", type=int, default=DPI) diff --git a/web/app.js b/web/app.js index 6d4c3e7..d625bd4 100644 --- a/web/app.js +++ b/web/app.js @@ -640,7 +640,7 @@ function renderResearch(data) { $("res-summary").textContent = "measurements unavailable"; $("res-caption").textContent = "This build carries no research.json — run tools/build_web.py with " + - "promo/rl-data.json present to populate it."; + ".docs/assets/rl-data.json present to populate it."; return; } const trained = (data.train_sizes || []).join("-"); diff --git a/web/index.html b/web/index.html index 21416d4..c8ca5e0 100644 --- a/web/index.html +++ b/web/index.html @@ -336,7 +336,7 @@

Read further

Roadmap 97-second video + href="https://github.com/openplan-labs/PythonPDDL/blob/main/.docs/assets/jupyddl-rl.mp4">97-second video

Train one yourself