demolab-cli 2.1.0__tar.gz → 2.2.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of demolab-cli might be problematic. Click here for more details.

Files changed (128) hide show
  1. demolab_cli-2.2.0/.gitattributes +1 -0
  2. {demolab_cli-2.1.0 → demolab_cli-2.2.0}/.github/workflows/publish.yml +2 -2
  3. {demolab_cli-2.1.0 → demolab_cli-2.2.0}/PKG-INFO +1 -1
  4. {demolab_cli-2.1.0 → demolab_cli-2.2.0}/demolab_cli/AGENT.md +2 -1
  5. {demolab_cli-2.1.0 → demolab_cli-2.2.0}/demolab_cli/CHANGELOG.md +27 -0
  6. demolab_cli-2.2.0/demolab_cli/VERSION +1 -0
  7. {demolab_cli-2.1.0 → demolab_cli-2.2.0}/demolab_cli/build.py +46 -8
  8. {demolab_cli-2.1.0 → demolab_cli-2.2.0}/demolab_cli/cli.py +5 -1
  9. {demolab_cli-2.1.0 → demolab_cli-2.2.0}/demolab_cli/devserver.py +1 -0
  10. {demolab_cli-2.1.0 → demolab_cli-2.2.0}/demolab_cli/guides/RULES.md +6 -1
  11. {demolab_cli-2.1.0 → demolab_cli-2.2.0}/demolab_cli/guides/STRUCTURE.md +1 -1
  12. {demolab_cli-2.1.0 → demolab_cli-2.2.0}/demolab_cli/runbooks/UPDATE.md +20 -0
  13. demolab_cli-2.2.0/demolab_cli/scaffold/skeleton/.gitattributes +1 -0
  14. {demolab_cli-2.1.0 → demolab_cli-2.2.0}/demolab_cli/test_cli.py +6 -0
  15. {demolab_cli-2.1.0 → demolab_cli-2.2.0}/demolab_cli/test_devserver.py +13 -0
  16. {demolab_cli-2.1.0 → demolab_cli-2.2.0}/demolab_cli/test_engine_build.py +34 -3
  17. demolab_cli-2.2.0/demolab_cli/typ/entry.typ +11 -0
  18. demolab_cli-2.1.0/demolab_cli/VERSION +0 -1
  19. {demolab_cli-2.1.0 → demolab_cli-2.2.0}/.github/workflows/landing-preview.yml +0 -0
  20. {demolab_cli-2.1.0 → demolab_cli-2.2.0}/.github/workflows/landing.yml +0 -0
  21. {demolab_cli-2.1.0 → demolab_cli-2.2.0}/.github/workflows/tests.yml +0 -0
  22. {demolab_cli-2.1.0 → demolab_cli-2.2.0}/.gitignore +0 -0
  23. {demolab_cli-2.1.0 → demolab_cli-2.2.0}/AGENTS.md +0 -0
  24. {demolab_cli-2.1.0 → demolab_cli-2.2.0}/CLAUDE.md +0 -0
  25. {demolab_cli-2.1.0 → demolab_cli-2.2.0}/DEVELOPING.md +0 -0
  26. {demolab_cli-2.1.0 → demolab_cli-2.2.0}/LICENSE +0 -0
  27. {demolab_cli-2.1.0 → demolab_cli-2.2.0}/README.md +0 -0
  28. {demolab_cli-2.1.0 → demolab_cli-2.2.0}/demolab_cli/__init__.py +0 -0
  29. {demolab_cli-2.1.0 → demolab_cli-2.2.0}/demolab_cli/_paths.py +0 -0
  30. {demolab_cli-2.1.0 → demolab_cli-2.2.0}/demolab_cli/deploy/deploy.yml +0 -0
  31. {demolab_cli-2.1.0 → demolab_cli-2.2.0}/demolab_cli/deploy/preview.yml +0 -0
  32. {demolab_cli-2.1.0 → demolab_cli-2.2.0}/demolab_cli/guides/AUTORESEARCH-RULES.md +0 -0
  33. {demolab_cli-2.1.0 → demolab_cli-2.2.0}/demolab_cli/guides/GLOSSARY.md +0 -0
  34. {demolab_cli-2.1.0 → demolab_cli-2.2.0}/demolab_cli/guides/HOUSESTYLE.md +0 -0
  35. {demolab_cli-2.1.0 → demolab_cli-2.2.0}/demolab_cli/guides/SLIDES.md +0 -0
  36. {demolab_cli-2.1.0 → demolab_cli-2.2.0}/demolab_cli/guides/SUPPORT.md +0 -0
  37. {demolab_cli-2.1.0 → demolab_cli-2.2.0}/demolab_cli/overlay.py +0 -0
  38. {demolab_cli-2.1.0 → demolab_cli-2.2.0}/demolab_cli/runbooks/AUTORESEARCH.md +0 -0
  39. {demolab_cli-2.1.0 → demolab_cli-2.2.0}/demolab_cli/runbooks/DOCTOR.md +0 -0
  40. {demolab_cli-2.1.0 → demolab_cli-2.2.0}/demolab_cli/runbooks/EMBED-DOCS.md +0 -0
  41. {demolab_cli-2.1.0 → demolab_cli-2.2.0}/demolab_cli/runbooks/FROM-JUPYTER.md +0 -0
  42. {demolab_cli-2.1.0 → demolab_cli-2.2.0}/demolab_cli/runbooks/FROM-PAPER.md +0 -0
  43. {demolab_cli-2.1.0 → demolab_cli-2.2.0}/demolab_cli/runbooks/GETTING-STARTED.md +0 -0
  44. {demolab_cli-2.1.0 → demolab_cli-2.2.0}/demolab_cli/runbooks/GROUND-CLAIMS.md +0 -0
  45. {demolab_cli-2.1.0 → demolab_cli-2.2.0}/demolab_cli/runbooks/LINT.md +0 -0
  46. {demolab_cli-2.1.0 → demolab_cli-2.2.0}/demolab_cli/runbooks/LITERATURE-SEARCH.md +0 -0
  47. {demolab_cli-2.1.0 → demolab_cli-2.2.0}/demolab_cli/runbooks/MIGRATE-CODE.md +0 -0
  48. {demolab_cli-2.1.0 → demolab_cli-2.2.0}/demolab_cli/runbooks/MIGRATE-STACK.md +0 -0
  49. {demolab_cli-2.1.0 → demolab_cli-2.2.0}/demolab_cli/runbooks/NEXT.md +0 -0
  50. {demolab_cli-2.1.0 → demolab_cli-2.2.0}/demolab_cli/runbooks/RED-TEAM.md +0 -0
  51. {demolab_cli-2.1.0 → demolab_cli-2.2.0}/demolab_cli/runbooks/STEELMAN.md +0 -0
  52. {demolab_cli-2.1.0 → demolab_cli-2.2.0}/demolab_cli/runbooks/TOUR.md +0 -0
  53. {demolab_cli-2.1.0 → demolab_cli-2.2.0}/demolab_cli/scaffold/demo/artifacts/data/ar018/swe-bench.svg +0 -0
  54. {demolab_cli-2.1.0 → demolab_cli-2.2.0}/demolab_cli/scaffold/demo/artifacts/pdfs/ar004.pdf +0 -0
  55. {demolab_cli-2.1.0 → demolab_cli-2.2.0}/demolab_cli/scaffold/demo/artifacts/pdfs/ar005.pdf +0 -0
  56. {demolab_cli-2.1.0 → demolab_cli-2.2.0}/demolab_cli/scaffold/demo/artifacts/pdfs/ar010.pdf +0 -0
  57. {demolab_cli-2.1.0 → demolab_cli-2.2.0}/demolab_cli/scaffold/demo/artifacts/pdfs/ar011.pdf +0 -0
  58. {demolab_cli-2.1.0 → demolab_cli-2.2.0}/demolab_cli/scaffold/demo/artifacts/pdfs/ar012.pdf +0 -0
  59. {demolab_cli-2.1.0 → demolab_cli-2.2.0}/demolab_cli/scaffold/demo/artifacts/pdfs/ar013.pdf +0 -0
  60. {demolab_cli-2.1.0 → demolab_cli-2.2.0}/demolab_cli/scaffold/demo/artifacts/pdfs/ar014.pdf +0 -0
  61. {demolab_cli-2.1.0 → demolab_cli-2.2.0}/demolab_cli/scaffold/demo/artifacts/pdfs/ar015.pdf +0 -0
  62. {demolab_cli-2.1.0 → demolab_cli-2.2.0}/demolab_cli/scaffold/demo/artifacts/pdfs/ar016.pdf +0 -0
  63. {demolab_cli-2.1.0 → demolab_cli-2.2.0}/demolab_cli/scaffold/demo/artifacts/pdfs/ar017.pdf +0 -0
  64. {demolab_cli-2.1.0 → demolab_cli-2.2.0}/demolab_cli/scaffold/demo/artifacts/pdfs/ar018.pdf +0 -0
  65. {demolab_cli-2.1.0 → demolab_cli-2.2.0}/demolab_cli/scaffold/demo/artifacts/pdfs/ar019.pdf +0 -0
  66. {demolab_cli-2.1.0 → demolab_cli-2.2.0}/demolab_cli/scaffold/demo/artifacts/pdfs/ar020.pdf +0 -0
  67. {demolab_cli-2.1.0 → demolab_cli-2.2.0}/demolab_cli/scaffold/demo/artifacts/pdfs/ar021.pdf +0 -0
  68. {demolab_cli-2.1.0 → demolab_cli-2.2.0}/demolab_cli/scaffold/demo/artifacts/pdfs/ar022.pdf +0 -0
  69. {demolab_cli-2.1.0 → demolab_cli-2.2.0}/demolab_cli/scaffold/demo/artifacts/pdfs/ar023.pdf +0 -0
  70. {demolab_cli-2.1.0 → demolab_cli-2.2.0}/demolab_cli/scaffold/demo/artifacts/pdfs/ar024.pdf +0 -0
  71. {demolab_cli-2.1.0 → demolab_cli-2.2.0}/demolab_cli/scaffold/demo/artifacts/pdfs/ar025.pdf +0 -0
  72. {demolab_cli-2.1.0 → demolab_cli-2.2.0}/demolab_cli/scaffold/demo/artifacts/pdfs/ar026.pdf +0 -0
  73. {demolab_cli-2.1.0 → demolab_cli-2.2.0}/demolab_cli/scaffold/demo/artifacts/pdfs/book.pdf +0 -0
  74. {demolab_cli-2.1.0 → demolab_cli-2.2.0}/demolab_cli/scaffold/demo/demolab.yaml +0 -0
  75. {demolab_cli-2.1.0 → demolab_cli-2.2.0}/demolab_cli/scaffold/demo/site/CNAME +0 -0
  76. {demolab_cli-2.1.0 → demolab_cli-2.2.0}/demolab_cli/scaffold/demo/site/landing.typ +0 -0
  77. {demolab_cli-2.1.0 → demolab_cli-2.2.0}/demolab_cli/scaffold/demo/writings/ar004.slide.typ +0 -0
  78. {demolab_cli-2.1.0 → demolab_cli-2.2.0}/demolab_cli/scaffold/demo/writings/ar005.slide.typ +0 -0
  79. {demolab_cli-2.1.0 → demolab_cli-2.2.0}/demolab_cli/scaffold/demo/writings/ar010.typ +0 -0
  80. {demolab_cli-2.1.0 → demolab_cli-2.2.0}/demolab_cli/scaffold/demo/writings/ar011.typ +0 -0
  81. {demolab_cli-2.1.0 → demolab_cli-2.2.0}/demolab_cli/scaffold/demo/writings/ar012.typ +0 -0
  82. {demolab_cli-2.1.0 → demolab_cli-2.2.0}/demolab_cli/scaffold/demo/writings/ar013.typ +0 -0
  83. {demolab_cli-2.1.0 → demolab_cli-2.2.0}/demolab_cli/scaffold/demo/writings/ar014.typ +0 -0
  84. {demolab_cli-2.1.0 → demolab_cli-2.2.0}/demolab_cli/scaffold/demo/writings/ar015.typ +0 -0
  85. {demolab_cli-2.1.0 → demolab_cli-2.2.0}/demolab_cli/scaffold/demo/writings/ar016.typ +0 -0
  86. {demolab_cli-2.1.0 → demolab_cli-2.2.0}/demolab_cli/scaffold/demo/writings/ar017.typ +0 -0
  87. {demolab_cli-2.1.0 → demolab_cli-2.2.0}/demolab_cli/scaffold/demo/writings/ar018.typ +0 -0
  88. {demolab_cli-2.1.0 → demolab_cli-2.2.0}/demolab_cli/scaffold/demo/writings/ar019.typ +0 -0
  89. {demolab_cli-2.1.0 → demolab_cli-2.2.0}/demolab_cli/scaffold/demo/writings/ar020.typ +0 -0
  90. {demolab_cli-2.1.0 → demolab_cli-2.2.0}/demolab_cli/scaffold/demo/writings/ar021.typ +0 -0
  91. {demolab_cli-2.1.0 → demolab_cli-2.2.0}/demolab_cli/scaffold/demo/writings/ar022.typ +0 -0
  92. {demolab_cli-2.1.0 → demolab_cli-2.2.0}/demolab_cli/scaffold/demo/writings/ar023.typ +0 -0
  93. {demolab_cli-2.1.0 → demolab_cli-2.2.0}/demolab_cli/scaffold/demo/writings/ar024.typ +0 -0
  94. {demolab_cli-2.1.0 → demolab_cli-2.2.0}/demolab_cli/scaffold/demo/writings/ar025.typ +0 -0
  95. {demolab_cli-2.1.0 → demolab_cli-2.2.0}/demolab_cli/scaffold/demo/writings/ar026.typ +0 -0
  96. {demolab_cli-2.1.0 → demolab_cli-2.2.0}/demolab_cli/scaffold/root/AGENTS.md +0 -0
  97. {demolab_cli-2.1.0 → demolab_cli-2.2.0}/demolab_cli/scaffold/root/CLAUDE.md +0 -0
  98. {demolab_cli-2.1.0 → demolab_cli-2.2.0}/demolab_cli/scaffold/root/README.md +0 -0
  99. {demolab_cli-2.1.0 → demolab_cli-2.2.0}/demolab_cli/scaffold/root/github/workflows/tests.yml +0 -0
  100. {demolab_cli-2.1.0 → demolab_cli-2.2.0}/demolab_cli/scaffold/root/gitignore +0 -0
  101. {demolab_cli-2.1.0 → demolab_cli-2.2.0}/demolab_cli/scaffold/root/pyproject.toml +0 -0
  102. {demolab_cli-2.1.0 → demolab_cli-2.2.0}/demolab_cli/scaffold/skeleton/HOUSESTYLE.local.md +0 -0
  103. {demolab_cli-2.1.0 → demolab_cli-2.2.0}/demolab_cli/scaffold/skeleton/artifacts/data/.gitkeep +0 -0
  104. {demolab_cli-2.1.0 → demolab_cli-2.2.0}/demolab_cli/scaffold/skeleton/artifacts/pdfs/.gitkeep +0 -0
  105. {demolab_cli-2.1.0 → demolab_cli-2.2.0}/demolab_cli/scaffold/skeleton/demolab.yaml +0 -0
  106. {demolab_cli-2.1.0 → demolab_cli-2.2.0}/demolab_cli/scaffold/skeleton/experiments/helpers/__init__.py +0 -0
  107. {demolab_cli-2.1.0 → demolab_cli-2.2.0}/demolab_cli/scaffold/skeleton/experiments/helpers/provenance.py +0 -0
  108. {demolab_cli-2.1.0 → demolab_cli-2.2.0}/demolab_cli/scaffold/skeleton/experiments/helpers/style.py +0 -0
  109. {demolab_cli-2.1.0 → demolab_cli-2.2.0}/demolab_cli/scaffold/skeleton/experiments/helpers/test_provenance.py +0 -0
  110. {demolab_cli-2.1.0 → demolab_cli-2.2.0}/demolab_cli/scaffold/skeleton/tools/.gitkeep +0 -0
  111. {demolab_cli-2.1.0 → demolab_cli-2.2.0}/demolab_cli/scaffold/skeleton/writings/.gitkeep +0 -0
  112. {demolab_cli-2.1.0 → demolab_cli-2.2.0}/demolab_cli/scaffold/starters/monte-carlo-pi/README.md +0 -0
  113. {demolab_cli-2.1.0 → demolab_cli-2.2.0}/demolab_cli/scaffold/starters/monte-carlo-pi/exp000.py +0 -0
  114. {demolab_cli-2.1.0 → demolab_cli-2.2.0}/demolab_cli/scaffold/starters/monte-carlo-pi/exp000.typ +0 -0
  115. {demolab_cli-2.1.0 → demolab_cli-2.2.0}/demolab_cli/slides.py +0 -0
  116. {demolab_cli-2.1.0 → demolab_cli-2.2.0}/demolab_cli/test_build_resilience.py +0 -0
  117. {demolab_cli-2.1.0 → demolab_cli-2.2.0}/demolab_cli/test_command_catalog.py +0 -0
  118. {demolab_cli-2.1.0 → demolab_cli-2.2.0}/demolab_cli/test_init_and_staging.py +0 -0
  119. {demolab_cli-2.1.0 → demolab_cli-2.2.0}/demolab_cli/test_overlay.py +0 -0
  120. {demolab_cli-2.1.0 → demolab_cli-2.2.0}/demolab_cli/test_slide_catalog.py +0 -0
  121. {demolab_cli-2.1.0 → demolab_cli-2.2.0}/demolab_cli/test_slides.py +0 -0
  122. {demolab_cli-2.1.0 → demolab_cli-2.2.0}/demolab_cli/typ/cite-popover.js +0 -0
  123. {demolab_cli-2.1.0 → demolab_cli-2.2.0}/demolab_cli/typ/favicon.svg +0 -0
  124. {demolab_cli-2.1.0 → demolab_cli-2.2.0}/demolab_cli/typ/lib.typ +0 -0
  125. {demolab_cli-2.1.0 → demolab_cli-2.2.0}/demolab_cli/typ/main.typ +0 -0
  126. {demolab_cli-2.1.0 → demolab_cli-2.2.0}/demolab_cli/typ/style.css +0 -0
  127. {demolab_cli-2.1.0 → demolab_cli-2.2.0}/pyproject.toml +0 -0
  128. {demolab_cli-2.1.0 → demolab_cli-2.2.0}/uv.lock +0 -0
@@ -0,0 +1 @@
1
+ *.pdf binary
@@ -45,8 +45,8 @@ jobs:
45
45
  env:
46
46
  TAG: v${{ steps.version.outputs.version }}
47
47
  run: |
48
- if git rev-parse "$TAG" >/dev/null 2>&1; then
49
- echo "$TAG already exists — skipping tag."
48
+ if git ls-remote --exit-code --tags origin "refs/tags/$TAG" >/dev/null 2>&1; then
49
+ echo "$TAG already exists on origin — skipping tag."
50
50
  else
51
51
  git tag "$TAG"
52
52
  git push origin "$TAG"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: demolab-cli
3
- Version: 2.1.0
3
+ Version: 2.2.0
4
4
  Summary: Lab notebook for computational science: Python tools produce per-run artifacts; Typst publishes them as a web bundle, per-entry PDFs, and a book.
5
5
  Project-URL: Homepage, https://github.com/eoinmurray/demolab
6
6
  Project-URL: Changelog, https://github.com/eoinmurray/demolab/blob/main/demolab_cli/CHANGELOG.md
@@ -71,7 +71,8 @@ guide covers it, ground your answer in it — cite it or offer to start it.
71
71
  `demolab` lists them all. The loop: `uv run python experiments/expNNN.py` (run an experiment
72
72
  end-to-end — run the runner directly, there's no `demolab run` wrapper) · `demolab dev`
73
73
  (live-preview server — have the *user* run it in their own terminal) · `demolab build`
74
- (site + PDFs) · `demolab test`. Reference data ships in the package:
74
+ (complete site + PDFs) · `demolab build <id>` (one entry PDF) · `demolab test`. Preview
75
+ PDFs stay in the ignored site tree; run the complete build before publishing. Reference data ships in the package:
75
76
  `demolab docs STARTERS` (canonical first experiments — model file shapes on them, never copy
76
77
  blindly; `monte-carlo-pi` is the default) and `demolab docs DEMO` (the published docs site's
77
78
  source).
@@ -13,6 +13,33 @@ the runbook shows the entries between your version and the latest.
13
13
 
14
14
  ## [Unreleased]
15
15
 
16
+ ## [2.2.0] — 2026-08-07
17
+
18
+ ### Added
19
+ - **Build one entry PDF with `demolab build <id>`.** The targeted build uses the same page
20
+ surface, branding, root, and artifact paths as the complete publication, while deliberately
21
+ leaving the website and combined book untouched.
22
+
23
+ ### Fixed
24
+ - **PDF builds are reproducible instead of changing metadata on every run.** Every Typst compile
25
+ now uses a documented `2000-01-01T00:00:00Z` creation timestamp by default and respects an
26
+ explicit `SOURCE_DATE_EPOCH` override. PDFs are marked binary in new labs and this source repo.
27
+ - **`demolab dev` no longer dirties committed publication PDFs.** Live preview still builds the
28
+ complete ignored website, including its PDF links, but no longer mirrors those preview PDFs into
29
+ `artifacts/pdfs/`; an explicit `demolab build` remains the publication step.
30
+
31
+ ## [2.1.1] — 2026-07-18
32
+
33
+ ### Fixed
34
+ - **UPDATE now verifies the project environment after upgrading `demolab-cli`.** The runbook
35
+ explains that uv's package upgrade already refreshes targeted cache metadata, compares the
36
+ active `uv run demolab` version with the lock, and uses a targeted package reinstall when they
37
+ disagree. It explicitly avoids global cache deletion and `uvx` verification, which operate at
38
+ the wrong scope for an existing lab.
39
+ - **Release tagging is idempotent against remote tags.** The publish workflow checks the remote
40
+ before pushing its release tag, avoiding a false-red release when the documented manual tag was
41
+ already pushed.
42
+
16
43
  ## [2.1.0] — 2026-07-18
17
44
 
18
45
  ### Added
@@ -0,0 +1 @@
1
+ 2.2.0
@@ -42,6 +42,7 @@ ROOT = Path(os.environ.get("DEMOLAB_ROOT") or _paths.find_lab_root() or Path.cwd
42
42
  WRITINGS = ROOT / "writings"
43
43
  BUILD = ROOT / "temp" / "bundle" # scratch: staged main.typ + manifest + deck PDFs
44
44
  MAIN = BUILD / "main.typ" # staged copy of the packaged typ/main.typ
45
+ ENTRY_MAIN = BUILD / "entry.typ" # staged single-entry PDF wrapper
45
46
  MANIFEST = BUILD / "index.json" # scratch: id/asset lists main.typ reads
46
47
  DECKS = BUILD / "decks" # scratch: compiled deck PDFs, embedded as assets
47
48
  SITE = ROOT / "artifacts" / "site" # bundle output (HTML + mp4 + pdfs/), gitignored
@@ -60,6 +61,13 @@ def _find_typst() -> str:
60
61
 
61
62
 
62
63
  TYPST = _find_typst()
64
+ DEFAULT_CREATION_TIMESTAMP = "946684800" # 2000-01-01T00:00:00Z
65
+
66
+
67
+ def typst_compile(*args: str) -> list[str]:
68
+ """A reproducible Typst compile command shared by every PDF-producing path."""
69
+ timestamp = os.environ.get("SOURCE_DATE_EPOCH", DEFAULT_CREATION_TIMESTAMP)
70
+ return [TYPST, "compile", "--creation-timestamp", timestamp, *args]
63
71
 
64
72
 
65
73
  def stage() -> None:
@@ -69,6 +77,7 @@ def stage() -> None:
69
77
  _paths.stage(ROOT)
70
78
  BUILD.mkdir(parents=True, exist_ok=True)
71
79
  shutil.copy2(_paths.TYP / "main.typ", MAIN)
80
+ shutil.copy2(_paths.TYP / "entry.typ", ENTRY_MAIN)
72
81
 
73
82
 
74
83
  def discover():
@@ -141,8 +150,8 @@ def compile_decks(deck_ids: list[str]) -> list[str]:
141
150
  good = []
142
151
  for d in deck_ids:
143
152
  proc = subprocess.run(
144
- [TYPST, "compile", "--root", str(ROOT),
145
- str(WRITINGS / f"{d}.slide.typ"), str(DECKS / f"{d}.pdf")],
153
+ typst_compile("--root", str(ROOT),
154
+ str(WRITINGS / f"{d}.slide.typ"), str(DECKS / f"{d}.pdf")),
146
155
  capture_output=True, text=True,
147
156
  )
148
157
  if proc.returncode == 0:
@@ -179,8 +188,8 @@ def compile_bundle(ids: list[str], deck_ids: list[str]) -> dict:
179
188
  while True:
180
189
  write_manifest(ids, deck_ids, broken=broken)
181
190
  proc = subprocess.run(
182
- [TYPST, "compile", "--format", "bundle", "--features", "bundle,html",
183
- "--root", str(ROOT), str(MAIN), str(SITE) + "/"],
191
+ typst_compile("--format", "bundle", "--features", "bundle,html",
192
+ "--root", str(ROOT), str(MAIN), str(SITE) + "/"),
184
193
  capture_output=True, text=True,
185
194
  )
186
195
  if proc.returncode == 0:
@@ -230,6 +239,26 @@ def _warn_if_content_misplaced(ids: list[str]) -> None:
230
239
  file=sys.stderr)
231
240
 
232
241
 
242
+ def compile_entry(entry_id: str, ids: list[str]) -> None:
243
+ """Compile one ordinary writing directly to its committed standalone PDF."""
244
+ if entry_id not in ids:
245
+ slide = WRITINGS / f"{entry_id}.slide.typ"
246
+ detail = " (it is a slide deck; use `demolab slides`)" if slide.exists() else ""
247
+ raise SystemExit(f"error: no buildable entry named {entry_id!r}{detail}")
248
+ PDFS.mkdir(parents=True, exist_ok=True)
249
+ proc = subprocess.run(
250
+ typst_compile("--root", str(ROOT), "--input", f"entry={entry_id}",
251
+ "--input", f"has-brand={str((ROOT / 'demolab.yaml').exists()).lower()}",
252
+ str(ENTRY_MAIN), str(PDFS / f"{entry_id}.pdf")),
253
+ capture_output=True, text=True,
254
+ )
255
+ if proc.returncode != 0:
256
+ sys.stderr.write(proc.stdout + proc.stderr)
257
+ raise subprocess.CalledProcessError(proc.returncode, proc.args, proc.stdout, proc.stderr)
258
+ print(f"built {entry_id} -> {PDFS.relative_to(ROOT)}/{entry_id}.pdf"
259
+ " (site and book not updated)", flush=True)
260
+
261
+
233
262
  def main() -> None:
234
263
  # --generate-only writes the manifest + deck PDFs without compiling the bundle: a hand
235
264
  # tool for inspecting what the compiler will see. (Dev serving is devserver.py, which runs
@@ -240,9 +269,16 @@ def main() -> None:
240
269
  # prose/CSS/lib edit doesn't pay for deck compilation it can't have affected. Safe only when
241
270
  # those PDFs exist (a full build ran first); a bare `demolab build` never skips.
242
271
  skip_decks = "--skip-decks" in sys.argv
272
+ no_mirror = "--no-mirror" in sys.argv
273
+ entry_args = [arg for arg in sys.argv[1:] if not arg.startswith("--")]
274
+ if len(entry_args) > 1:
275
+ raise SystemExit("error: build accepts at most one entry id")
243
276
  stage()
244
277
  ids = discover()
245
278
  _warn_if_content_misplaced(ids)
279
+ if entry_args:
280
+ compile_entry(entry_args[0], ids)
281
+ return
246
282
  deck_ids = discover_decks()
247
283
  # Zero writings is a valid state (a freshly `demolab scaffold`-ed repo): main.typ renders
248
284
  # a friendly empty-state homepage, so we build rather than error.
@@ -263,9 +299,10 @@ def main() -> None:
263
299
  broken = compile_bundle(ids, good_decks)
264
300
  good = [i for i in ids if i not in broken]
265
301
  # mirror the compiled PDFs (entries, book, and decks) to the committed artifacts/pdfs/
266
- PDFS.mkdir(parents=True, exist_ok=True)
267
- for pdf in sorted((SITE / "pdfs").glob("*.pdf")):
268
- shutil.copy(pdf, PDFS / pdf.name)
302
+ if not no_mirror:
303
+ PDFS.mkdir(parents=True, exist_ok=True)
304
+ for pdf in sorted((SITE / "pdfs").glob("*.pdf")):
305
+ shutil.copy(pdf, PDFS / pdf.name)
269
306
  # The verbose detail (which ids built / stubbed, where the PDFs mirror) goes on its own line;
270
307
  # the CONCISE summary is printed LAST, because the dev-server watch loop echoes only build.py's
271
308
  # final stdout line on each rebuild. So a `demolab dev` session shows a terse one-liner, while a
@@ -273,7 +310,8 @@ def main() -> None:
273
310
  print(f" entries: {', '.join(good)}"
274
311
  + (f" · decks: {', '.join(good_decks)}" if good_decks else "")
275
312
  + (f" · ⚠ stubbed: {', '.join(sorted(broken))}" if broken else "")
276
- + f" · pdfs -> {PDFS.relative_to(ROOT)}/")
313
+ + (f" · pdfs -> {PDFS.relative_to(ROOT)}/" if not no_mirror
314
+ else " · preview PDFs left untracked"))
277
315
  summary = f"built {len(good)} entries" + (f" + {len(good_decks)} decks" if good_decks else "")
278
316
  if broken:
279
317
  summary += f", {len(broken)} stubbed"
@@ -303,7 +303,8 @@ def cmd_dev(args: argparse.Namespace) -> int:
303
303
 
304
304
 
305
305
  def cmd_build(args: argparse.Namespace) -> int:
306
- return _mod("build", cwd=_paths.require_lab_root())
306
+ entry_args = [args.entry] if args.entry else []
307
+ return _mod("build", *entry_args, cwd=_paths.require_lab_root())
307
308
 
308
309
 
309
310
  def cmd_slides(args: argparse.Namespace) -> int:
@@ -379,6 +380,9 @@ def _build_parser() -> argparse.ArgumentParser:
379
380
  sp.add_argument("--demo", action="store_true", help="serve the shipped demo instead of your lab")
380
381
  sp.add_argument("--landing", action="store_true",
381
382
  help="with --demo: preview the upstream landing page too")
383
+ elif name == "build":
384
+ sp.add_argument("entry", nargs="?",
385
+ help="build only this entry PDF; omit for the complete publication")
382
386
  return p
383
387
 
384
388
 
@@ -172,6 +172,7 @@ def build(skip_decks: bool = False) -> tuple[bool, str]:
172
172
  DEMOLAB_ROOT pins the child to this server's ROOT (which may itself be an override —
173
173
  `demolab dev --demo` serves a materialised scratch lab)."""
174
174
  cmd = [sys.executable, "-m", "demolab_cli.build"]
175
+ cmd.append("--no-mirror")
175
176
  if skip_decks:
176
177
  cmd.append("--skip-decks")
177
178
  try:
@@ -13,7 +13,7 @@ defined in [`GLOSSARY.md`](GLOSSARY.md).
13
13
 
14
14
  **1.1 — Python via `uv`.** Never call `python` / `python3` directly. Deps are pinned in the root `pyproject.toml` / `uv.lock`; run scripts with `uv run python <script>` (e.g. `uv run python tools/neuron/tool.py lif`); `uv sync` after pulling.
15
15
 
16
- **1.2 — Publishing via `typst`.** Use the `typst` CLI (an installed prerequisite, alongside `uv`). It compiles the site + PDFs (`demolab build` / `demolab dev`); the bundle build passes `--features bundle,html` (experimental, deliberately used here). No Node/`bun` — demolab publishes entirely with Typst.
16
+ **1.2 — Publishing via `typst`.** Use the `typst` CLI (an installed prerequisite, alongside `uv`). It compiles the site + PDFs (`demolab build` / `demolab dev`); the bundle build passes `--features bundle,html` (experimental, deliberately used here). Typst receives a deterministic creation timestamp (`SOURCE_DATE_EPOCH` overrides the engine default), so unchanged PDFs stay byte-identical within a stable toolchain. No Node/`bun` — demolab publishes entirely with Typst.
17
17
 
18
18
  **1.3 — Prefer `demolab`.** Common commands are exposed through the `demolab` CLI — prefer `demolab <name>` (run `demolab` to list them). To check the toolchain *and* that the repo obeys these conventions, run the *Doctor the repo* runbook (*"doctor the repo"*).
19
19
 
@@ -77,6 +77,11 @@ The concrete annotated file tree is in [`STRUCTURE.md`](STRUCTURE.md); this sect
77
77
  - **Per-entry PDFs** — `artifacts/site/pdfs/<id>.pdf`.
78
78
  - **Book** — `artifacts/site/pdfs/book.pdf`: every entry, with a table of contents.
79
79
 
80
+ For a PDF-only editing loop, `demolab build <id>` updates just
81
+ `artifacts/pdfs/<id>.pdf`; it deliberately leaves the site and `book.pdf` untouched.
82
+ `demolab dev` refreshes the complete ignored preview site without mirroring preview PDFs into
83
+ the tracked publication directory. Run bare `demolab build` before publishing.
84
+
80
85
  **5.3 — What's committed.** PDFs are mirrored to the committed, shareable `artifacts/pdfs/`. `artifacts/site/` is a gitignored build output (CI regenerates + deploys it to Pages). CI does **not** run the experiments, so `artifacts/data/` **must** be committed — that record, not the ephemeral `temp/`, is what reaches the site.
81
86
 
82
87
  **5.4 — Numbers can't drift.** Each `writings/<id>.typ` reads its own bundle natively — `json("/artifacts/data/<id>/numbers.json")`, `#image("/artifacts/data/<id>/fig.png")` (compiled with `--root` at the repo root) — so the numbers and figures come straight from the run.
@@ -67,7 +67,7 @@ lives in the installed `demolab-cli` package (site-packages), reached via `demol
67
67
 
68
68
  **S5 — The package vs your stuff.** The engine is the installed `demolab-cli` package — you never hand-edit it, and updating it is a dependency bump (§3.1). The gitignored `.demolab/` staging dir is the CLI's, not yours (§3.2). Everything else in the tree is yours: `tools/`, `experiments/`, `writings/`, `artifacts/`, `demolab.yaml`, and the root stubs `demolab init` laid down (§3.3).
69
69
 
70
- **S6 — Where the build goes.** `demolab build` globs `writings/*.typ` into `temp/bundle/index.json`, stages the engine's `main.typ` beside it, and compiles it to three targets in one pass: the web site → `artifacts/site/`, per-entry PDFs + `book.pdf` → `artifacts/site/pdfs/` (mirrored to `artifacts/pdfs/`). On an empty (freshly-initialised) tree the build still succeeds — it emits a single `index.html` with a friendly empty state, and skips `all.html`/collection pages/`book.pdf`. CI deploys `artifacts/site/` to GitHub Pages (§5.2, §5.3).
70
+ **S6 — Where the build goes.** `demolab build` globs `writings/*.typ` into `temp/bundle/index.json`, stages the engine's `main.typ` beside it, and compiles it to three targets in one pass: the web site → `artifacts/site/`, per-entry PDFs + `book.pdf` → `artifacts/site/pdfs/` (mirrored to `artifacts/pdfs/`). `demolab build <id>` instead updates only `artifacts/pdfs/<id>.pdf`; `demolab dev` keeps preview PDFs in the ignored site tree and never mirrors them. On an empty (freshly-initialised) tree the complete build still succeeds — it emits a single `index.html` with a friendly empty state, and skips `all.html`/collection pages/`book.pdf`. CI deploys `artifacts/site/` to GitHub Pages (§5.2, §5.3).
71
71
 
72
72
  ## Scaffolding
73
73
 
@@ -36,6 +36,26 @@ updating the engine can't touch the user's identity — that's the point.
36
36
  uv lock --upgrade-package demolab-cli && uv sync
37
37
  ```
38
38
 
39
+ `--upgrade-package demolab-cli` already implies uv's targeted
40
+ `--refresh-package demolab-cli`; do not clear uv's whole cache. After syncing, verify that the
41
+ project environment, not a globally cached or `uvx`-managed copy, is active:
42
+
43
+ ```sh
44
+ uv run demolab version
45
+ uv tree --locked | rg 'demolab-cli'
46
+ ```
47
+
48
+ The versions must agree. If they do not, repair only this package and verify again:
49
+
50
+ ```sh
51
+ uv sync --reinstall-package demolab-cli
52
+ uv run demolab version
53
+ ```
54
+
55
+ Stop and report the mismatch if it persists; never paper over it with `uv cache clean`, and do
56
+ not use `uvx demolab-cli` to verify an existing lab because that is a separate ephemeral
57
+ environment.
58
+
39
59
  4. **Rebuild.** `demolab build` — the CLI notices the version change, refreshes `.demolab/`,
40
60
  and prints a one-line notice pointing at the changelog. `demolab test` for good measure.
41
61
 
@@ -38,6 +38,12 @@ def test_dev_arguments():
38
38
  assert bare_dev.port is None and bare_dev.demo is False and bare_dev.landing is False
39
39
 
40
40
 
41
+ def test_build_accepts_optional_entry():
42
+ parser = cli._build_parser()
43
+ assert parser.parse_args(["build"]).entry is None
44
+ assert parser.parse_args(["build", "exp007"]).entry == "exp007"
45
+
46
+
41
47
  def test_demo_landing_passes_live_source_to_devserver(tmp_path, monkeypatch):
42
48
  scaffold = tmp_path / "package-scaffold"
43
49
  (scaffold / "skeleton").mkdir(parents=True)
@@ -7,6 +7,7 @@ HTTP request, no build) that an IPv4-only server gets wrong on Windows.
7
7
  """
8
8
  import socket
9
9
  import threading
10
+ from types import SimpleNamespace
10
11
 
11
12
  from demolab_cli import devserver
12
13
 
@@ -84,6 +85,18 @@ def test_deck_affecting_triggers_on_slide_and_data():
84
85
  assert not devserver.deck_affecting(set())
85
86
 
86
87
 
88
+ def test_dev_build_never_mirrors_preview_pdfs(monkeypatch):
89
+ captured = {}
90
+
91
+ def run(cmd, **kwargs):
92
+ captured["cmd"] = cmd
93
+ return SimpleNamespace(returncode=0, stdout="built\n", stderr="")
94
+
95
+ monkeypatch.setattr(devserver.subprocess, "run", run)
96
+ assert devserver.build() == (True, "built")
97
+ assert "--no-mirror" in captured["cmd"]
98
+
99
+
87
100
  def test_sync_landing_source_copies_updates_and_removal(tmp_path):
88
101
  source = tmp_path / "package" / "site" / "landing.typ"
89
102
  target = tmp_path / "preview" / "landing.typ"
@@ -9,6 +9,7 @@ empty-state homepage rather than erroring.
9
9
 
10
10
  Needs the `typst` CLI on PATH (same as `demolab build`); skipped if it's missing.
11
11
  """
12
+ import hashlib
12
13
  import os
13
14
  import re
14
15
  import shutil
@@ -18,7 +19,7 @@ from pathlib import Path
18
19
 
19
20
  import pytest
20
21
 
21
- from demolab_cli import _paths
22
+ from demolab_cli import _paths, build
22
23
 
23
24
  SCAFFOLD = _paths.SCAFFOLD
24
25
 
@@ -40,14 +41,44 @@ def _assemble(root: Path, *, demo: bool) -> None:
40
41
  ignore=shutil.ignore_patterns("landing.typ", "temp", "site"))
41
42
 
42
43
 
43
- def _build(root: Path) -> None:
44
+ def _build(root: Path, entry: str | None = None) -> None:
44
45
  subprocess.run(
45
- [sys.executable, "-m", "demolab_cli.build"],
46
+ [sys.executable, "-m", "demolab_cli.build", *([entry] if entry else [])],
46
47
  env={**os.environ, "DEMOLAB_ROOT": str(root)},
47
48
  check=True,
48
49
  )
49
50
 
50
51
 
52
+ def _sha256(path: Path) -> str:
53
+ return hashlib.sha256(path.read_bytes()).hexdigest()
54
+
55
+
56
+ def test_complete_build_is_pdf_reproducible(tmp_path: Path) -> None:
57
+ root = tmp_path / "repo"
58
+ root.mkdir()
59
+ _assemble(root, demo=True)
60
+ _build(root)
61
+ first = {p.name: _sha256(p) for p in (root / "artifacts" / "pdfs").glob("*.pdf")}
62
+ _build(root)
63
+ second = {p.name: _sha256(p) for p in (root / "artifacts" / "pdfs").glob("*.pdf")}
64
+ assert second == first
65
+
66
+
67
+ def test_targeted_build_writes_only_selected_pdf(tmp_path: Path) -> None:
68
+ root = tmp_path / "repo"
69
+ root.mkdir()
70
+ _assemble(root, demo=True)
71
+ shutil.rmtree(root / "artifacts" / "pdfs", ignore_errors=True)
72
+ _build(root, "ar018")
73
+ assert sorted(p.name for p in (root / "artifacts" / "pdfs").glob("*.pdf")) == ["ar018.pdf"]
74
+ assert not (root / "artifacts" / "site").exists()
75
+
76
+
77
+ def test_source_date_epoch_overrides_default(tmp_path: Path, monkeypatch) -> None:
78
+ monkeypatch.setenv("SOURCE_DATE_EPOCH", "1234567890")
79
+ assert build.typst_compile("input.typ")[3] == "1234567890"
80
+
81
+
51
82
  def test_demo_fixture_builds_full_site(tmp_path: Path) -> None:
52
83
  root = tmp_path / "repo"
53
84
  root.mkdir()
@@ -0,0 +1,11 @@
1
+ // Standalone entry-PDF root used by `demolab build <id>`.
2
+ #import "/.demolab/lib.typ": *
3
+
4
+ #let entry-id = sys.inputs.entry
5
+ #let has-brand = sys.inputs.at("has-brand", default: "false") == "true"
6
+ #let config = if has-brand { yaml("/demolab.yaml") } else { (:) }
7
+ #let brand = default-brand + config
8
+ #import "/writings/" + entry-id + ".typ" as writing
9
+
10
+ #set document(title: writing.meta.title)
11
+ #numbered-pages(entry-page(writing.meta, writing.body, id: entry-id, brand: brand))
@@ -1 +0,0 @@
1
- 2.1.0
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes