unaltraweb 0.3.0

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.
Files changed (444) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE +20 -0
  3. data/Makefile +412 -0
  4. data/README.md +226 -0
  5. data/_config.yml +842 -0
  6. data/_data/i18n/ca.yml +278 -0
  7. data/_data/i18n/en.yml +278 -0
  8. data/_data/i18n/es.yml +278 -0
  9. data/_includes/audio.liquid +30 -0
  10. data/_includes/bib_search.liquid +5 -0
  11. data/_includes/blog-list.liquid +66 -0
  12. data/_includes/book-citation.liquid +33 -0
  13. data/_includes/citation.liquid +26 -0
  14. data/_includes/custom_head.liquid +3 -0
  15. data/_includes/custom_scripts.liquid +5 -0
  16. data/_includes/cv/list.liquid +5 -0
  17. data/_includes/cv/list_groups.liquid +51 -0
  18. data/_includes/cv/map.liquid +29 -0
  19. data/_includes/cv/nested_list.liquid +16 -0
  20. data/_includes/cv/time_table.liquid +118 -0
  21. data/_includes/cv-download-card.liquid +26 -0
  22. data/_includes/developer-mode-switcher.liquid +24 -0
  23. data/_includes/disqus.liquid +13 -0
  24. data/_includes/distill_scripts.liquid +298 -0
  25. data/_includes/documentation-download-detail.liquid +76 -0
  26. data/_includes/documentation-downloads.liquid +71 -0
  27. data/_includes/documentation-profile-attributes.liquid +4 -0
  28. data/_includes/documentation-section-toc.liquid +9 -0
  29. data/_includes/documentation-sidebar.liquid +112 -0
  30. data/_includes/figure.liquid +87 -0
  31. data/_includes/footer.liquid +66 -0
  32. data/_includes/giscus.liquid +25 -0
  33. data/_includes/head.liquid +220 -0
  34. data/_includes/header.liquid +544 -0
  35. data/_includes/latest_posts.liquid +61 -0
  36. data/_includes/localized-date.liquid +5 -0
  37. data/_includes/manual-bibliography.liquid +2 -0
  38. data/_includes/manual-featured-readings.liquid +12 -0
  39. data/_includes/manual-other-readings.liquid +12 -0
  40. data/_includes/manual-page-toc.liquid +11 -0
  41. data/_includes/manual-sidebar.liquid +58 -0
  42. data/_includes/manual-teachers.liquid +74 -0
  43. data/_includes/metadata.liquid +278 -0
  44. data/_includes/news.liquid +48 -0
  45. data/_includes/newsletter.liquid +68 -0
  46. data/_includes/page-hero.liquid +96 -0
  47. data/_includes/page-link.liquid +8 -0
  48. data/_includes/pagination.liquid +22 -0
  49. data/_includes/post-reading-time.liquid +29 -0
  50. data/_includes/previous-next.liquid +37 -0
  51. data/_includes/profile-card.liquid +49 -0
  52. data/_includes/profile-feature-matrix.liquid +75 -0
  53. data/_includes/profile-highlights.liquid +122 -0
  54. data/_includes/project-grid.liquid +26 -0
  55. data/_includes/project-resource-link.liquid +45 -0
  56. data/_includes/project-resources.liquid +21 -0
  57. data/_includes/projects.liquid +38 -0
  58. data/_includes/projects_horizontal.liquid +37 -0
  59. data/_includes/publications-metrics-summary.liquid +69 -0
  60. data/_includes/publications-summary.liquid +127 -0
  61. data/_includes/reading-biblio-controls.liquid +65 -0
  62. data/_includes/reading-cover-card.liquid +93 -0
  63. data/_includes/reading-rating.liquid +11 -0
  64. data/_includes/recommended-readings.liquid +30 -0
  65. data/_includes/related_posts.liquid +39 -0
  66. data/_includes/repository/repo.liquid +48 -0
  67. data/_includes/repository/repo_trophies.liquid +44 -0
  68. data/_includes/repository/repo_user.liquid +35 -0
  69. data/_includes/resume/awards.liquid +19 -0
  70. data/_includes/resume/basics.liquid +28 -0
  71. data/_includes/resume/certificates.liquid +35 -0
  72. data/_includes/resume/education.liquid +54 -0
  73. data/_includes/resume/interests.liquid +34 -0
  74. data/_includes/resume/languages.liquid +31 -0
  75. data/_includes/resume/projects.liquid +32 -0
  76. data/_includes/resume/publications.liquid +28 -0
  77. data/_includes/resume/references.liquid +31 -0
  78. data/_includes/resume/skills.liquid +33 -0
  79. data/_includes/resume/volunteer.liquid +52 -0
  80. data/_includes/resume/work.liquid +52 -0
  81. data/_includes/scripts.liquid +364 -0
  82. data/_includes/search-data.js.liquid +341 -0
  83. data/_includes/selected_papers.liquid +3 -0
  84. data/_includes/t.liquid +31 -0
  85. data/_includes/team-grid.liquid +172 -0
  86. data/_includes/video.liquid +97 -0
  87. data/_layouts/about.liquid +93 -0
  88. data/_layouts/archive.liquid +45 -0
  89. data/_layouts/bib.liquid +677 -0
  90. data/_layouts/book-review.liquid +225 -0
  91. data/_layouts/book-shelf.liquid +66 -0
  92. data/_layouts/cv.liquid +127 -0
  93. data/_layouts/default.liquid +70 -0
  94. data/_layouts/distill.liquid +132 -0
  95. data/_layouts/documentation-download-detail.liquid +5 -0
  96. data/_layouts/documentation-home.liquid +100 -0
  97. data/_layouts/documentation-page.liquid +45 -0
  98. data/_layouts/manual-bib.liquid +73 -0
  99. data/_layouts/manual-chapter.liquid +99 -0
  100. data/_layouts/manual-featured-bib.liquid +100 -0
  101. data/_layouts/manual-home.liquid +120 -0
  102. data/_layouts/none.liquid +1 -0
  103. data/_layouts/outputs.liquid +29 -0
  104. data/_layouts/page.liquid +52 -0
  105. data/_layouts/post.liquid +95 -0
  106. data/_layouts/profile.liquid +36 -0
  107. data/_layouts/profiles.liquid +36 -0
  108. data/_layouts/project.liquid +7 -0
  109. data/_layouts/repositories.liquid +58 -0
  110. data/_layouts/theses.liquid +126 -0
  111. data/_plugins/bibliography_profiles.rb +164 -0
  112. data/_plugins/callouts.rb +104 -0
  113. data/_plugins/code_blocks.rb +277 -0
  114. data/_plugins/computation_figure_images.rb +266 -0
  115. data/_plugins/computation_sources.rb +42 -0
  116. data/_plugins/content_search_index.rb +210 -0
  117. data/_plugins/details.rb +23 -0
  118. data/_plugins/external-posts.rb +124 -0
  119. data/_plugins/figure_captions.rb +800 -0
  120. data/_plugins/file-exists.rb +22 -0
  121. data/_plugins/google-scholar-citations.rb +86 -0
  122. data/_plugins/hide-custom-bibtex.rb +18 -0
  123. data/_plugins/inspirehep-citations.rb +57 -0
  124. data/_plugins/localized_visual_sources.rb +145 -0
  125. data/_plugins/manual_release_metadata.rb +47 -0
  126. data/_plugins/mermaid_mmd_images.rb +233 -0
  127. data/_plugins/profile-pages.rb +28 -0
  128. data/_plugins/remove-accents.rb +32 -0
  129. data/_plugins/reproducible_build_time.rb +55 -0
  130. data/_plugins/search-data.rb +83 -0
  131. data/_plugins/theme-cache-bust.rb +56 -0
  132. data/_plugins/vega_visual_images.rb +290 -0
  133. data/_plugins/web_capture_images.rb +154 -0
  134. data/_sass/_base.scss +2133 -0
  135. data/_sass/_blog.scss +67 -0
  136. data/_sass/_callouts.scss +100 -0
  137. data/_sass/_custom.scss +437 -0
  138. data/_sass/_cv.scss +274 -0
  139. data/_sass/_developer.scss +97 -0
  140. data/_sass/_distill.scss +185 -0
  141. data/_sass/_documentation.scss +2220 -0
  142. data/_sass/_layout.scss +218 -0
  143. data/_sass/_manual.scss +2691 -0
  144. data/_sass/_profile.scss +323 -0
  145. data/_sass/_projects.scss +163 -0
  146. data/_sass/_reading.scss +465 -0
  147. data/_sass/_site-custom.scss +3 -0
  148. data/_sass/_tabs.scss +48 -0
  149. data/_sass/_team.scss +230 -0
  150. data/_sass/_themes.scss +254 -0
  151. data/_sass/_typograms.scss +132 -0
  152. data/_sass/_variables.scss +52 -0
  153. data/_sass/font-awesome/_animated.scss +152 -0
  154. data/_sass/font-awesome/_bordered-pulled.scss +20 -0
  155. data/_sass/font-awesome/_core.scss +49 -0
  156. data/_sass/font-awesome/_fixed-width.scss +7 -0
  157. data/_sass/font-awesome/_functions.scss +57 -0
  158. data/_sass/font-awesome/_icons.scss +13 -0
  159. data/_sass/font-awesome/_list.scss +18 -0
  160. data/_sass/font-awesome/_mixins.scss +65 -0
  161. data/_sass/font-awesome/_rotated-flipped.scss +31 -0
  162. data/_sass/font-awesome/_screen-reader.scss +14 -0
  163. data/_sass/font-awesome/_shims.scss +1578 -0
  164. data/_sass/font-awesome/_sizing.scss +16 -0
  165. data/_sass/font-awesome/_stacked.scss +32 -0
  166. data/_sass/font-awesome/_variables.scss +5044 -0
  167. data/_sass/font-awesome/brands.scss +30 -0
  168. data/_sass/font-awesome/fontawesome.scss +21 -0
  169. data/_sass/font-awesome/regular.scss +27 -0
  170. data/_sass/font-awesome/solid.scss +27 -0
  171. data/_sass/font-awesome/v4-shims.scss +11 -0
  172. data/_sass/tabler-icons/tabler-icons-filled.scss +1646 -0
  173. data/_sass/tabler-icons/tabler-icons-outline.scss +9859 -0
  174. data/_sass/tabler-icons/tabler-icons.scss +11463 -0
  175. data/_scripts/cronitor-analytics-setup.js +9 -0
  176. data/_scripts/giscus-setup.js +48 -0
  177. data/_scripts/google-analytics-setup.js +9 -0
  178. data/_scripts/open-panel-analytics-setup.js +14 -0
  179. data/_scripts/photoswipe-setup.js +11 -0
  180. data/assets/audio/epicaly-short-113909.mp3 +0 -0
  181. data/assets/bibliography/2018-12-22-distill.bib +7 -0
  182. data/assets/css/academicons.min.css +1 -0
  183. data/assets/css/bootstrap-toc.min.css +4 -0
  184. data/assets/css/bootstrap.min.css +7 -0
  185. data/assets/css/bootstrap.min.css.map +1 -0
  186. data/assets/css/jekyll-pygments-themes-github.css +209 -0
  187. data/assets/css/jekyll-pygments-themes-native.css +232 -0
  188. data/assets/css/jupyter-grade3.css +3758 -0
  189. data/assets/css/jupyter-monokai.css +3758 -0
  190. data/assets/css/jupyter.css +383 -0
  191. data/assets/css/main.scss +36 -0
  192. data/assets/css/mdb.min.css +48 -0
  193. data/assets/css/mdb.min.css.map +1 -0
  194. data/assets/css/scholar-icons.css +386 -0
  195. data/assets/fonts/academicons.ttf +0 -0
  196. data/assets/fonts/academicons.woff +0 -0
  197. data/assets/fonts/scholar-icons.ttf +0 -0
  198. data/assets/fonts/scholar-icons.woff +0 -0
  199. data/assets/fonts/tabler-icons-filled.ttf +0 -0
  200. data/assets/fonts/tabler-icons-filled.woff +0 -0
  201. data/assets/fonts/tabler-icons-filled.woff2 +0 -0
  202. data/assets/fonts/tabler-icons-outline.ttf +0 -0
  203. data/assets/fonts/tabler-icons-outline.woff +0 -0
  204. data/assets/fonts/tabler-icons-outline.woff2 +0 -0
  205. data/assets/fonts/tabler-icons.ttf +0 -0
  206. data/assets/fonts/tabler-icons.woff +0 -0
  207. data/assets/fonts/tabler-icons.woff2 +0 -0
  208. data/assets/html/relativity.html +15 -0
  209. data/assets/img/1.jpg +0 -0
  210. data/assets/img/10.jpg +0 -0
  211. data/assets/img/11.jpg +0 -0
  212. data/assets/img/12.jpg +0 -0
  213. data/assets/img/2.jpg +0 -0
  214. data/assets/img/3.jpg +0 -0
  215. data/assets/img/4.jpg +0 -0
  216. data/assets/img/5.jpg +0 -0
  217. data/assets/img/6.jpg +0 -0
  218. data/assets/img/7.jpg +0 -0
  219. data/assets/img/8.jpg +0 -0
  220. data/assets/img/9.jpg +0 -0
  221. data/assets/img/book_covers/the_godfather.jpg +0 -0
  222. data/assets/img/brand/dosquartsdedocs-logo-cafe.svg +31 -0
  223. data/assets/img/brand/dosquartsdedocs-logo-footer-cafe.svg +31 -0
  224. data/assets/img/brand/dosquartsdedocs-logo-white.svg +31 -0
  225. data/assets/img/brand/dosquartsdedocs-logo.svg +31 -0
  226. data/assets/img/prof_pic.jpg +0 -0
  227. data/assets/img/prof_pic_color.png +0 -0
  228. data/assets/img/profile-placeholder.svg +8 -0
  229. data/assets/img/publication_preview/brownian-motion.gif +0 -0
  230. data/assets/img/publication_preview/goodchild1992-gis-cover.jpg +0 -0
  231. data/assets/img/publication_preview/gutierrez2020-jtrg-preview.jpg +0 -0
  232. data/assets/img/publication_preview/harvey1989-postmodernity-cover.jpg +0 -0
  233. data/assets/img/publication_preview/tobler1970-economic-geography-cover.jpg +0 -0
  234. data/assets/img/publication_preview/tuan1977-space-place-cover.jpg +0 -0
  235. data/assets/img/publication_preview/wave-mechanics.gif +0 -0
  236. data/assets/img/publication_preview/zaragozi2025-rooftop-pv-preview.jpg +0 -0
  237. data/assets/img/rhino.png +0 -0
  238. data/assets/img/team/aaron-gutierrez.png +0 -0
  239. data/assets/img/team/alba-font.png +0 -0
  240. data/assets/img/team/alba-viana.png +0 -0
  241. data/assets/img/team/alejandro-gonzalez.png +0 -0
  242. data/assets/img/team/ana-pastor.png +0 -0
  243. data/assets/img/team/antoni-domenech.png +0 -0
  244. data/assets/img/team/benito-zaragozi.png +0 -0
  245. data/assets/img/team/cinta-sanz.png +0 -0
  246. data/assets/img/team/daniel-miravet.png +0 -0
  247. data/assets/img/team/eugenia-altamirano.png +0 -0
  248. data/assets/img/team/fiammetta-brandajs.png +0 -0
  249. data/assets/img/team/francesc-gonzalez.png +0 -0
  250. data/assets/img/team/isabel-paulino.png +0 -0
  251. data/assets/img/team/joan-borras.png +0 -0
  252. data/assets/img/team/julie-wilson.png +0 -0
  253. data/assets/img/team/liliana-sole.png +0 -0
  254. data/assets/img/team/maria-casado.png +0 -0
  255. data/assets/img/team/maria-medina.png +0 -0
  256. data/assets/img/team/marta-nello.png +0 -0
  257. data/assets/img/team/mogrify +1 -0
  258. data/assets/img/team/oscar-saladie.png +0 -0
  259. data/assets/img/team/paolo-russo.png +0 -0
  260. data/assets/img/team/raquel-santos.png +0 -0
  261. data/assets/img/team/salvador-anton.png +0 -0
  262. data/assets/img/team/sunsi-huertas.png +0 -0
  263. data/assets/img/team/user-placeholder.png +0 -0
  264. data/assets/img/team/wilbert-den-hoed.png +0 -0
  265. data/assets/img/team/xavier-delclos.png +0 -0
  266. data/assets/img/template_error.png +0 -0
  267. data/assets/js/bibsearch.js +70 -0
  268. data/assets/js/bootstrap-toc.min.js +5 -0
  269. data/assets/js/bootstrap.bundle.min.js +7 -0
  270. data/assets/js/bootstrap.bundle.min.js.map +1 -0
  271. data/assets/js/callouts.js +115 -0
  272. data/assets/js/chartjs-setup.js +14 -0
  273. data/assets/js/common.js +117 -0
  274. data/assets/js/content-search-match.js +159 -0
  275. data/assets/js/content-search.js +441 -0
  276. data/assets/js/copy_code.js +60 -0
  277. data/assets/js/custom.js +6 -0
  278. data/assets/js/developer-mode.js +103 -0
  279. data/assets/js/diff2html-setup.js +20 -0
  280. data/assets/js/distillpub/overrides.js +27 -0
  281. data/assets/js/distillpub/template.v2.js +9616 -0
  282. data/assets/js/distillpub/template.v2.js.map +1 -0
  283. data/assets/js/distillpub/transforms.v2.js +14557 -0
  284. data/assets/js/distillpub/transforms.v2.js.map +1 -0
  285. data/assets/js/documentation.js +435 -0
  286. data/assets/js/echarts-setup.js +29 -0
  287. data/assets/js/highlight-search-term.js +110 -0
  288. data/assets/js/jupyter_new_tab.js +18 -0
  289. data/assets/js/leaflet-setup.js +22 -0
  290. data/assets/js/manual.js +308 -0
  291. data/assets/js/masonry.js +12 -0
  292. data/assets/js/mathjax-setup.js +46 -0
  293. data/assets/js/mermaid-setup.js +37 -0
  294. data/assets/js/newsletter.js +105 -0
  295. data/assets/js/no_defer.js +24 -0
  296. data/assets/js/plotly-setup.js +52 -0
  297. data/assets/js/profile-nav.js +92 -0
  298. data/assets/js/progress-bar.js +73 -0
  299. data/assets/js/pseudocode-setup.js +18 -0
  300. data/assets/js/search/base-styles.min.js +286 -0
  301. data/assets/js/search/command-score.min.js +7 -0
  302. data/assets/js/search/hotkeys-js/hotkeys.esm.min.js +18 -0
  303. data/assets/js/search/lit/decorators.min.js +7 -0
  304. data/assets/js/search/lit/directives/class-map.min.js +7 -0
  305. data/assets/js/search/lit/directives/join.min.js +7 -0
  306. data/assets/js/search/lit/directives/live.min.js +7 -0
  307. data/assets/js/search/lit/directives/ref.min.js +7 -0
  308. data/assets/js/search/lit/directives/repeat.min.js +7 -0
  309. data/assets/js/search/lit/directives/unsafe-html.min.js +7 -0
  310. data/assets/js/search/lit/index.min.js +7 -0
  311. data/assets/js/search/lit-element/lit-element.min.js +12 -0
  312. data/assets/js/search/lit-html/directives/class-map.min.js +22 -0
  313. data/assets/js/search/lit-html/directives/join.min.js +12 -0
  314. data/assets/js/search/lit-html/directives/live.min.js +28 -0
  315. data/assets/js/search/lit-html/directives/ref.min.js +32 -0
  316. data/assets/js/search/lit-html/directives/repeat.min.js +27 -0
  317. data/assets/js/search/lit-html/directives/unsafe-html.min.js +22 -0
  318. data/assets/js/search/lit-html/is-server.min.js +12 -0
  319. data/assets/js/search/lit-html/lit-html.min.js +12 -0
  320. data/assets/js/search/ninja-action.min.js +101 -0
  321. data/assets/js/search/ninja-footer.min.js +62 -0
  322. data/assets/js/search/ninja-header.min.js +78 -0
  323. data/assets/js/search/ninja-keys.min.js +39 -0
  324. data/assets/js/search/reactive-element/decorators/custom-element.min.js +12 -0
  325. data/assets/js/search/reactive-element/decorators/event-options.min.js +12 -0
  326. data/assets/js/search/reactive-element/decorators/property.min.js +22 -0
  327. data/assets/js/search/reactive-element/decorators/query-all.min.js +17 -0
  328. data/assets/js/search/reactive-element/decorators/query-assigned-elements.min.js +17 -0
  329. data/assets/js/search/reactive-element/decorators/query-assigned-nodes.min.js +17 -0
  330. data/assets/js/search/reactive-element/decorators/query-async.min.js +17 -0
  331. data/assets/js/search/reactive-element/decorators/query.min.js +17 -0
  332. data/assets/js/search/reactive-element/decorators/state.min.js +27 -0
  333. data/assets/js/search/reactive-element/reactive-element.min.js +17 -0
  334. data/assets/js/search-setup.js +17 -0
  335. data/assets/js/shortcut-key.js +11 -0
  336. data/assets/js/tabs.min.js +5 -0
  337. data/assets/js/theme.js +314 -0
  338. data/assets/js/tooltips-setup.js +3 -0
  339. data/assets/js/typograms.js +1341 -0
  340. data/assets/js/vanilla-back-to-top.min.js +1 -0
  341. data/assets/js/vega-setup.js +24 -0
  342. data/assets/js/venobox-setup.js +5 -0
  343. data/assets/js/wechat.js +14 -0
  344. data/assets/js/zoom.js +15 -0
  345. data/assets/json/resume.json +98 -0
  346. data/assets/json/table_data.json +128 -0
  347. data/assets/jupyter/blog.ipynb +48 -0
  348. data/assets/pdf/example_pdf.pdf +0 -0
  349. data/assets/plotly/demo.html +72 -0
  350. data/assets/video/pexels-engin-akyurt-6069112-960x540-30fps.mp4 +0 -0
  351. data/assets/webfonts/fa-brands-400.ttf +0 -0
  352. data/assets/webfonts/fa-brands-400.woff2 +0 -0
  353. data/assets/webfonts/fa-regular-400.ttf +0 -0
  354. data/assets/webfonts/fa-regular-400.woff2 +0 -0
  355. data/assets/webfonts/fa-solid-900.ttf +0 -0
  356. data/assets/webfonts/fa-solid-900.woff2 +0 -0
  357. data/assets/webfonts/fa-v4compatibility.ttf +0 -0
  358. data/assets/webfonts/fa-v4compatibility.woff2 +0 -0
  359. data/docs/Gemfile +5 -0
  360. data/docs/_config.yml +83 -0
  361. data/docs/_documentation/en/01-quick-start.md +115 -0
  362. data/docs/_documentation/en/02-tools.md +146 -0
  363. data/docs/_documentation/en/03-usage.md +107 -0
  364. data/docs/_documentation/en/04-jekyll-basics.md +51 -0
  365. data/docs/_documentation/en/05-pages-collections.md +68 -0
  366. data/docs/_documentation/en/06-github-web-editing.md +92 -0
  367. data/docs/_documentation/en/10-profiles.md +125 -0
  368. data/docs/_documentation/en/11-unaltreselfie.md +39 -0
  369. data/docs/_documentation/en/12-unaltreprojecte.md +38 -0
  370. data/docs/_documentation/en/13-unaltremanual.md +237 -0
  371. data/docs/_documentation/en/14-unaltredocs.md +92 -0
  372. data/docs/_documentation/en/15-profile-checklist.md +39 -0
  373. data/docs/_documentation/en/20-syntax.md +353 -0
  374. data/docs/_documentation/en/21-themes.md +103 -0
  375. data/docs/_documentation/en/22-feature-reference.md +56 -0
  376. data/docs/_documentation/en/23-front-matter.md +51 -0
  377. data/docs/_documentation/en/24-data-files.md +28 -0
  378. data/docs/_documentation/en/30-customization.md +448 -0
  379. data/docs/_documentation/en/31-template.md +55 -0
  380. data/docs/_documentation/en/32-development.md +108 -0
  381. data/docs/_documentation/en/33-reusable-workflows.md +136 -0
  382. data/docs/_documentation/en/40-distribution.md +164 -0
  383. data/docs/_documentation/en/41-bibliometrics.md +137 -0
  384. data/docs/_documentation/en/42-docker-image.md +185 -0
  385. data/docs/_documentation/en/50-faq.md +33 -0
  386. data/docs/_pages/ca/index.md +35 -0
  387. data/docs/_pages/en/index.md +35 -0
  388. data/docs/_pages/es/index.md +35 -0
  389. data/docs/agents/action-prompts/00-start-site-session.txt +10 -0
  390. data/docs/agents/action-prompts/05-create-new-web.txt +5 -0
  391. data/docs/agents/action-prompts/10-content-update.txt +9 -0
  392. data/docs/agents/action-prompts/15-edit-default-content.txt +8 -0
  393. data/docs/agents/action-prompts/20-manual-teaching-materials.txt +18 -0
  394. data/docs/agents/action-prompts/22-manual-style-audit.txt +14 -0
  395. data/docs/agents/action-prompts/23-manual-structure-audit.txt +17 -0
  396. data/docs/agents/action-prompts/25-translation-prepublish.txt +8 -0
  397. data/docs/agents/action-prompts/30-project-site-update.txt +7 -0
  398. data/docs/agents/action-prompts/40-documentation-update.txt +7 -0
  399. data/docs/agents/action-prompts/50-bibliography-entry.txt +7 -0
  400. data/docs/agents/action-prompts/60-bibliometrics-refresh.txt +7 -0
  401. data/docs/agents/action-prompts/70-build-and-review.txt +12 -0
  402. data/docs/agents/manual-authoring-components.md +255 -0
  403. data/docs/agents/mcp-contract.md +162 -0
  404. data/docs/agents/quarto-computation-adoption.md +102 -0
  405. data/docs/assets/img/unaltraweb-docs-hero.svg +36 -0
  406. data/lib/unaltraweb/version.rb +8 -0
  407. data/lib/unaltraweb.rb +31 -0
  408. data/requirements.txt +5 -0
  409. data/scripts/biblio/fetch_scimago_csv.sh +126 -0
  410. data/scripts/biblio/metrics_common.py +154 -0
  411. data/scripts/biblio/metrics_merge_meta.py +73 -0
  412. data/scripts/biblio/metrics_update.py +826 -0
  413. data/scripts/computations/python/Dockerfile +37 -0
  414. data/scripts/computations/r/Dockerfile +31 -0
  415. data/scripts/computations/render.py +1267 -0
  416. data/scripts/cv/render_pdf_preview.sh +36 -0
  417. data/scripts/deploy/publish_branch.sh +80 -0
  418. data/scripts/generate_logo_variants.py +47 -0
  419. data/scripts/manual/Dockerfile +31 -0
  420. data/scripts/manual/build_pdf.py +1780 -0
  421. data/scripts/manual/filters/bibliography.lua +164 -0
  422. data/scripts/manual/filters/code-blocks.lua +102 -0
  423. data/scripts/manual/filters/figure-captions.lua +35 -0
  424. data/scripts/manual/publish_release.sh +281 -0
  425. data/scripts/manual/templates/manual.tex +494 -0
  426. data/scripts/manual/verify_release_assets.py +650 -0
  427. data/scripts/test_gem_build.py +128 -0
  428. data/scripts/test_reproducible_jekyll_build.py +191 -0
  429. data/scripts/test_wheel_install.py +282 -0
  430. data/scripts/unaltraweb-docker-mount.sh +36 -0
  431. data/scripts/unaltraweb-mcp-bootstrap.sh +105 -0
  432. data/scripts/unaltraweb-mcp-cleanup.sh +73 -0
  433. data/scripts/unaltraweb-mcp-project-id.sh +34 -0
  434. data/scripts/validate_distribution.py +563 -0
  435. data/scripts/validate_workflows.py +1285 -0
  436. data/scripts/web_captures/Dockerfile +31 -0
  437. data/scripts/web_captures/capture.mjs +157 -0
  438. data/scripts/web_captures/package-lock.json +56 -0
  439. data/scripts/web_captures/package.json +9 -0
  440. data/scripts/web_captures/render.py +867 -0
  441. data/src/unaltraweb_mcp/component-contract.json +153 -0
  442. data/src/unaltraweb_mcp/component-contract.schema.json +87 -0
  443. data/src/unaltraweb_mcp/docker_mount.py +19 -0
  444. metadata +861 -0
@@ -0,0 +1,128 @@
1
+ #!/usr/bin/env python3
2
+ """Build the gem without network access and inspect its packaged contract files."""
3
+
4
+ from __future__ import annotations
5
+
6
+ import io
7
+ import json
8
+ import shutil
9
+ import subprocess
10
+ import sys
11
+ import tarfile
12
+ import tempfile
13
+ from pathlib import Path
14
+
15
+
16
+ ROOT = Path(__file__).resolve().parents[1]
17
+ sys.path.insert(0, str(ROOT / "src"))
18
+
19
+ from unaltraweb_mcp.docker_mount import docker_bind_mount
20
+
21
+
22
+ CONTRACT = json.loads((ROOT / "src/unaltraweb_mcp/component-contract.json").read_text(encoding="utf-8"))
23
+ RUNTIME_IMAGE = str(CONTRACT["components"]["runtime"]["reference"])
24
+
25
+
26
+ def run(command: list[str], *, cwd: Path = ROOT) -> subprocess.CompletedProcess[str]:
27
+ completed = subprocess.run(
28
+ command,
29
+ cwd=cwd,
30
+ text=True,
31
+ stdout=subprocess.PIPE,
32
+ stderr=subprocess.PIPE,
33
+ check=False,
34
+ )
35
+ if completed.returncode != 0:
36
+ raise RuntimeError(f"Command failed: {' '.join(command)}\n{completed.stdout}\n{completed.stderr}")
37
+ return completed
38
+
39
+
40
+ def build(output: Path, *, source: Path = ROOT) -> str:
41
+ if shutil.which("ruby") and shutil.which("gem"):
42
+ run(["gem", "build", "unaltraweb.gemspec", "--output", str(output)], cwd=source)
43
+ return "local-ruby"
44
+ if not shutil.which("docker"):
45
+ raise RuntimeError("Gem smoke requires local RubyGems or Docker with the selected runtime image.")
46
+ inspected = subprocess.run(
47
+ ["docker", "image", "inspect", RUNTIME_IMAGE],
48
+ stdout=subprocess.DEVNULL,
49
+ stderr=subprocess.DEVNULL,
50
+ check=False,
51
+ )
52
+ if inspected.returncode != 0:
53
+ raise RuntimeError(f"Gem smoke requires the already-local runtime image {RUNTIME_IMAGE}; it never pulls implicitly.")
54
+ run([
55
+ "docker", "run", "--rm", "--network", "none",
56
+ "--mount", docker_bind_mount(source, "/repo", readonly=True),
57
+ "--mount", docker_bind_mount(output.parent, "/out"), "-w", "/repo",
58
+ RUNTIME_IMAGE, "gem", "build", "unaltraweb.gemspec", "--output", f"/out/{output.name}",
59
+ ])
60
+ return "docker-runtime"
61
+
62
+
63
+ def inspect_gem(path: Path) -> None:
64
+ with tarfile.open(path, mode="r") as package:
65
+ data_member = package.extractfile("data.tar.gz")
66
+ if data_member is None:
67
+ raise RuntimeError("Built gem has no data.tar.gz payload.")
68
+ with tarfile.open(fileobj=io.BytesIO(data_member.read()), mode="r:gz") as payload:
69
+ names = set(payload.getnames())
70
+ required = {
71
+ "LICENSE",
72
+ "README.md",
73
+ "_data/i18n/ca.yml",
74
+ "_data/i18n/en.yml",
75
+ "_data/i18n/es.yml",
76
+ "_plugins/bibliography_profiles.rb",
77
+ "_plugins/code_blocks.rb",
78
+ "_plugins/content_search_index.rb",
79
+ "_plugins/figure_captions.rb",
80
+ "_plugins/manual_release_metadata.rb",
81
+ "_plugins/reproducible_build_time.rb",
82
+ "assets/js/content-search-match.js",
83
+ "assets/js/content-search.js",
84
+ "_config.yml",
85
+ "Makefile",
86
+ "requirements.txt",
87
+ "lib/unaltraweb/version.rb",
88
+ "scripts/manual/build_pdf.py",
89
+ "scripts/manual/filters/bibliography.lua",
90
+ "scripts/manual/filters/code-blocks.lua",
91
+ "scripts/manual/filters/figure-captions.lua",
92
+ "scripts/manual/publish_release.sh",
93
+ "scripts/manual/verify_release_assets.py",
94
+ "scripts/unaltraweb-docker-mount.sh",
95
+ "scripts/unaltraweb-mcp-cleanup.sh",
96
+ "scripts/unaltraweb-mcp-project-id.sh",
97
+ "src/unaltraweb_mcp/component-contract.json",
98
+ "src/unaltraweb_mcp/component-contract.schema.json",
99
+ "src/unaltraweb_mcp/docker_mount.py",
100
+ }
101
+ missing = sorted(required - names)
102
+ if missing:
103
+ raise RuntimeError(f"Built gem is missing required files: {missing}")
104
+ unexpected_data = sorted(name for name in names if name.startswith("_data/") and not name.startswith("_data/i18n/"))
105
+ if unexpected_data:
106
+ raise RuntimeError(f"Built gem contains non-runtime data files: {unexpected_data}")
107
+
108
+
109
+ def main() -> int:
110
+ with tempfile.TemporaryDirectory(prefix="unaltraweb-gem-") as raw_temp:
111
+ output = Path(raw_temp) / "unaltraweb.gem"
112
+ runner = build(output)
113
+ inspect_gem(output)
114
+ archive_source = Path(raw_temp) / "source-archive"
115
+ shutil.copytree(
116
+ ROOT,
117
+ archive_source,
118
+ ignore=shutil.ignore_patterns(".git", "_site", "tmp", ".jekyll-cache", "__pycache__", "*.pyc"),
119
+ )
120
+ archive_output = Path(raw_temp) / "unaltraweb-source-archive.gem"
121
+ build(archive_output, source=archive_source)
122
+ inspect_gem(archive_output)
123
+ print(json.dumps({"ok": True, "runner": runner, "gem": output.name}, sort_keys=True))
124
+ return 0
125
+
126
+
127
+ if __name__ == "__main__":
128
+ raise SystemExit(main())
@@ -0,0 +1,191 @@
1
+ #!/usr/bin/env python3
2
+ """Prove deterministic Jekyll output with the packaged core and fixed epoch."""
3
+
4
+ from __future__ import annotations
5
+
6
+ import hashlib
7
+ import json
8
+ import os
9
+ import stat
10
+ import subprocess
11
+ import tempfile
12
+ from pathlib import Path
13
+
14
+
15
+ ROOT = Path(__file__).resolve().parents[1]
16
+ IMAGE = os.environ.get("DOCKER_IMAGE", "ghcr.io/dosquartsdedocs/unaltraweb:0.3.0")
17
+ SELECTOR = "v2026.09"
18
+ EPOCH = "946684800"
19
+
20
+
21
+ def run(command: list[str], *, check: bool = True) -> subprocess.CompletedProcess[str]:
22
+ completed = subprocess.run(
23
+ command,
24
+ cwd=ROOT,
25
+ text=True,
26
+ stdout=subprocess.PIPE,
27
+ stderr=subprocess.PIPE,
28
+ check=False,
29
+ )
30
+ if check and completed.returncode != 0:
31
+ detail = completed.stderr.strip() or completed.stdout.strip()
32
+ raise RuntimeError(f"Command failed ({completed.returncode}): {' '.join(command)}\n{detail}")
33
+ return completed
34
+
35
+
36
+ def docker_build(site: Path, destination: str, epoch: str) -> subprocess.CompletedProcess[str]:
37
+ return run(
38
+ [
39
+ "docker",
40
+ "run",
41
+ "--rm",
42
+ "--network",
43
+ "none",
44
+ "--user",
45
+ f"{os.getuid()}:{os.getgid()}",
46
+ "-e",
47
+ "HOME=/tmp",
48
+ "-e",
49
+ "BUNDLE_GEMFILE=/site/Gemfile.local",
50
+ "-e",
51
+ "JEKYLL_ENV=production",
52
+ "-e",
53
+ "LANG=C.UTF-8",
54
+ "-e",
55
+ "LC_ALL=C.UTF-8",
56
+ "-e",
57
+ f"SOURCE_DATE_EPOCH={epoch}",
58
+ "-e",
59
+ "TZ=UTC",
60
+ "-e",
61
+ f"UNALTRAWEB_MANUAL_RELEASE_SELECTOR={SELECTOR}",
62
+ "--mount",
63
+ f"type=bind,src={ROOT},dst=/core,readonly",
64
+ "--mount",
65
+ f"type=bind,src={site},dst=/site",
66
+ "--workdir",
67
+ "/site",
68
+ "--entrypoint",
69
+ "/bin/sh",
70
+ IMAGE,
71
+ "-c",
72
+ "umask 022; bundle lock --local && bundle check && "
73
+ "bundle exec jekyll build --config /core/_config.yml,/site/_config.yml "
74
+ f"--destination /site/{destination} --disable-disk-cache",
75
+ ],
76
+ check=False,
77
+ )
78
+
79
+
80
+ def tree_inventory(root: Path) -> dict[str, dict[str, int | str]]:
81
+ result: dict[str, dict[str, int | str]] = {}
82
+ for path in sorted(root.rglob("*")):
83
+ relative = path.relative_to(root).as_posix()
84
+ metadata = path.lstat()
85
+ if stat.S_ISLNK(metadata.st_mode) or not (stat.S_ISDIR(metadata.st_mode) or stat.S_ISREG(metadata.st_mode)):
86
+ raise RuntimeError(f"Unexpected output type in reproducibility fixture: {relative}")
87
+ if path.is_dir():
88
+ result[relative + "/"] = {"mode": stat.S_IMODE(metadata.st_mode), "type": "directory"}
89
+ else:
90
+ content = path.read_bytes()
91
+ result[relative] = {
92
+ "mode": stat.S_IMODE(metadata.st_mode),
93
+ "sha256": hashlib.sha256(content).hexdigest(),
94
+ "size": len(content),
95
+ "type": "file",
96
+ }
97
+ return result
98
+
99
+
100
+ def set_source_mtime(site: Path, timestamp: int) -> None:
101
+ for path in site.rglob("*"):
102
+ if not path.is_dir() and not any(part.startswith("_site-") for part in path.parts):
103
+ os.utime(path, (timestamp, timestamp))
104
+
105
+
106
+ def write_fixture(site: Path) -> None:
107
+ (site / "_posts").mkdir(parents=True)
108
+ (site / "Gemfile.local").write_text(
109
+ 'source "https://rubygems.org"\ngem "unaltraweb", path: "/core"\n',
110
+ encoding="utf-8",
111
+ )
112
+ (site / "_config.yml").write_text(
113
+ "title: Reproducible manual\n"
114
+ "url: https://example.invalid\n"
115
+ "baseurl: ''\n"
116
+ "lang: en\n"
117
+ "default_lang: en\n"
118
+ "languages: [en]\n"
119
+ "theme: unaltraweb\n"
120
+ "plugins: [unaltraweb]\n"
121
+ "last_updated: true\n"
122
+ "exclude: [_site-a, _site-b, _site-invalid]\n"
123
+ "unaltraweb:\n"
124
+ " site_profile: unaltremanual\n"
125
+ " manual:\n"
126
+ " pdf:\n"
127
+ " enabled: false\n",
128
+ encoding="utf-8",
129
+ )
130
+ (site / "index.md").write_text(
131
+ "---\ntitle: Manual\n---\n{% include footer.liquid %}\n",
132
+ encoding="utf-8",
133
+ )
134
+ (site / "_posts/1999-12-31-reviewed.md").write_text(
135
+ "---\ntitle: Reviewed\ndate: 1999-12-31 12:00:00 +0000\n---\nReviewed post.\n",
136
+ encoding="utf-8",
137
+ )
138
+ (site / "manual.pdf").write_bytes(b"reviewed static file\n")
139
+
140
+
141
+ def main() -> int:
142
+ inspected = run(["docker", "image", "inspect", IMAGE], check=False)
143
+ if inspected.returncode != 0:
144
+ raise RuntimeError(f"Reproducibility check requires the already-local runtime image {IMAGE}; it never pulls implicitly.")
145
+
146
+ with tempfile.TemporaryDirectory(prefix="unaltraweb-reproducible-site-") as raw:
147
+ site = Path(raw).resolve()
148
+ write_fixture(site)
149
+ set_source_mtime(site, 978307200)
150
+ first = docker_build(site, "_site-a", EPOCH)
151
+ if first.returncode != 0:
152
+ raise RuntimeError(first.stderr.strip() or first.stdout.strip())
153
+
154
+ set_source_mtime(site, 1893456000)
155
+ second = docker_build(site, "_site-b", EPOCH)
156
+ if second.returncode != 0:
157
+ raise RuntimeError(second.stderr.strip() or second.stdout.strip())
158
+
159
+ first_inventory = tree_inventory(site / "_site-a")
160
+ second_inventory = tree_inventory(site / "_site-b")
161
+ if first_inventory != second_inventory:
162
+ differing = sorted(set(first_inventory) | set(second_inventory))
163
+ differing = [path for path in differing if first_inventory.get(path) != second_inventory.get(path)]
164
+ raise RuntimeError("Fixed-epoch Jekyll builds differ: " + ", ".join(differing[:12]))
165
+
166
+ expected_marker = json.dumps(
167
+ {"channel": "stable", "schema_version": 1, "selector": SELECTOR},
168
+ ensure_ascii=False,
169
+ indent=2,
170
+ sort_keys=True,
171
+ ) + "\n"
172
+ marker = (site / "_site-a/manual-release.json").read_text(encoding="utf-8")
173
+ if marker != expected_marker:
174
+ raise RuntimeError("manual-release.json was not preserved as canonical JSON.")
175
+ index = (site / "_site-a/index.html").read_text(encoding="utf-8")
176
+ if "2000" not in index or "January 1, 2000" not in index:
177
+ raise RuntimeError("Footer does not use the fixed build time.")
178
+ feed = (site / "_site-a/feed.xml").read_text(encoding="utf-8")
179
+ if "2000-01-01T00:00:00+00:00" not in feed:
180
+ raise RuntimeError("Feed does not use the fixed build time.")
181
+
182
+ invalid = docker_build(site, "_site-invalid", "not-an-epoch")
183
+ if invalid.returncode == 0 or "SOURCE_DATE_EPOCH" not in (invalid.stderr + invalid.stdout):
184
+ raise RuntimeError("Malformed SOURCE_DATE_EPOCH did not fail the Jekyll build.")
185
+
186
+ print(json.dumps({"image": IMAGE, "ok": True, "selector": SELECTOR}, sort_keys=True))
187
+ return 0
188
+
189
+
190
+ if __name__ == "__main__":
191
+ raise SystemExit(main())
@@ -0,0 +1,282 @@
1
+ #!/usr/bin/env python3
2
+ """Build and exercise a clean, factory-free unaltraweb-mcp wheel install."""
3
+
4
+ from __future__ import annotations
5
+
6
+ import json
7
+ import os
8
+ import shutil
9
+ import subprocess
10
+ import sys
11
+ import tempfile
12
+ import venv
13
+ import zipfile
14
+ from pathlib import Path
15
+
16
+
17
+ ROOT = Path(__file__).resolve().parents[1]
18
+
19
+
20
+ def run(command: list[str], *, cwd: Path, env: dict[str, str] | None = None, expected: int = 0) -> subprocess.CompletedProcess[str]:
21
+ completed = subprocess.run(command, cwd=cwd, env=env, text=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE, check=False)
22
+ if completed.returncode != expected:
23
+ raise RuntimeError(
24
+ f"Command returned {completed.returncode}, expected {expected}: {' '.join(command)}\n{completed.stdout}\n{completed.stderr}"
25
+ )
26
+ return completed
27
+
28
+
29
+ def main() -> int:
30
+ with tempfile.TemporaryDirectory(prefix="unaltraweb-wheel-") as raw_temp:
31
+ temp = Path(raw_temp)
32
+ source = temp / "source"
33
+ shutil.copytree(
34
+ ROOT,
35
+ source,
36
+ ignore=shutil.ignore_patterns(
37
+ ".git",
38
+ "build",
39
+ "dist",
40
+ "*.egg-info",
41
+ "_site",
42
+ "tmp",
43
+ "__pycache__",
44
+ "*.pyc",
45
+ ),
46
+ )
47
+ wheel_dir = temp / "dist"
48
+ wheel_dir.mkdir()
49
+ env = os.environ.copy()
50
+ env["PIP_NO_INDEX"] = "1"
51
+ run(
52
+ [sys.executable, "-m", "pip", "wheel", ".", "--no-deps", "--no-build-isolation", "--wheel-dir", str(wheel_dir)],
53
+ cwd=source,
54
+ env=env,
55
+ )
56
+ wheels = list(wheel_dir.glob("unaltraweb_mcp-*.whl"))
57
+ if len(wheels) != 1:
58
+ raise RuntimeError(f"Expected one wheel, found: {wheels}")
59
+ with zipfile.ZipFile(wheels[0]) as archive:
60
+ names = archive.namelist()
61
+ forbidden_roots = {"docs", "scripts", "_layouts", "_includes", "_sass"}
62
+ leaked = []
63
+ for name in names:
64
+ parts = Path(name).parts
65
+ if not parts or parts[0] != "unaltraweb_mcp":
66
+ continue
67
+ package_parts = parts[1:]
68
+ if package_parts and (package_parts[0] in forbidden_roots or package_parts[0] in {"mcp-factory.yml", "Dockerfile", "Makefile"}):
69
+ leaked.append(name)
70
+ if leaked:
71
+ raise RuntimeError(f"Factory assets leaked into wheel: {leaked}")
72
+ required = [
73
+ "unaltraweb_mcp/calibre_import.py",
74
+ "unaltraweb_mcp/component-contract.json",
75
+ "unaltraweb_mcp/component-contract.schema.json",
76
+ "unaltraweb_mcp/manual_release.py",
77
+ "unaltraweb_mcp/scaffolds/common/AGENTS.md.tmpl",
78
+ "unaltraweb_mcp/scaffolds/common/Makefile.tmpl",
79
+ "unaltraweb_mcp/scaffolds/common/README.md.tmpl",
80
+ "unaltraweb_mcp/scaffolds/common/.gitignore.tmpl",
81
+ "unaltraweb_mcp/scaffolds/common/.unaltraweb/docker-mount.sh",
82
+ "unaltraweb_mcp/scaffolds/common/root.html.tmpl",
83
+ "unaltraweb_mcp/scaffolds/profiles/unaltremanual/computations.yml.tmpl",
84
+ "unaltraweb_mcp/scaffolds/profiles/unaltremanual/_bibliography/manual.bib",
85
+ "unaltraweb_mcp/scaffolds/profiles/unaltreprojecte/_bibliography/papers.bib",
86
+ "unaltraweb_mcp/scaffolds/profiles/unaltreselfie/_bibliography/papers.bib",
87
+ "unaltraweb_mcp/scaffolds/common/.github/CONTRIBUTING.md",
88
+ "unaltraweb_mcp/scaffolds/common/.github/dependabot.yml.tmpl",
89
+ "unaltraweb_mcp/scaffolds/common/.github/pull_request_template.md",
90
+ "unaltraweb_mcp/scaffolds/common/.github/workflows/deploy.yml.tmpl",
91
+ ]
92
+ missing = [name for name in required if name not in names]
93
+ if missing:
94
+ raise RuntimeError(f"Wheel is missing modular package assets: {missing}")
95
+ if "unaltraweb_mcp/scaffolds/common/.gitignore" in names:
96
+ raise RuntimeError("Wheel contains the retired static common .gitignore")
97
+
98
+ environment = temp / "venv"
99
+ venv.EnvBuilder(with_pip=True).create(environment)
100
+ python = environment / "bin/python"
101
+ cli = environment / "bin/unaltraweb-mcp"
102
+ run([str(python), "-m", "pip", "install", "--no-deps", str(wheels[0])], cwd=temp, env=env)
103
+ version = run([str(cli), "version"], cwd=temp).stdout.strip()
104
+ doctor = json.loads(run([str(cli), "doctor"], cwd=temp).stdout)
105
+ if not doctor["ok"] or doctor["mode"] != "wheel" or not doctor["limited"]:
106
+ raise RuntimeError(f"Unexpected clean wheel doctor result: {doctor}")
107
+
108
+ contract = json.loads((ROOT / "src/unaltraweb_mcp/component-contract.json").read_text(encoding="utf-8"))
109
+ integration = contract["consumer_integration"]
110
+ profiles = {
111
+ "unaltreselfie": ("Personal academic or professional site.", "`_posts/` and `_news/`"),
112
+ "unaltreprojecte": ("Research project, group, infrastructure, or output site.", "`_data/team.yml`"),
113
+ "unaltremanual": ("Book-like manual, course, or teaching site.", "`_chapters/<lang>/`"),
114
+ "unaltredocs": ("Technical or operational documentation portal.", "`_documentation/<lang>/`"),
115
+ }
116
+ sites: dict[str, Path] = {}
117
+ for profile, (description, profile_path) in profiles.items():
118
+ profile_site = temp / f"site-{profile}"
119
+ created = json.loads(run([
120
+ str(cli), "--project", str(profile_site), "new-web", "--site-profile", profile,
121
+ "--title", f"Wheel {profile}",
122
+ ], cwd=temp).stdout)
123
+ if not created["ok"]:
124
+ raise RuntimeError(f"new-web failed for {profile} from clean wheel: {created}")
125
+ readme = (profile_site / "README.md").read_text(encoding="utf-8")
126
+ collaboration = (profile_site / ".github/CONTRIBUTING.md").read_text(encoding="utf-8")
127
+ dependabot = (profile_site / ".github/dependabot.yml").read_text(encoding="utf-8")
128
+ pr_template = (profile_site / ".github/pull_request_template.md").read_text(encoding="utf-8")
129
+ if not readme.startswith("# GitHub Web Editing Workflow\n"):
130
+ raise RuntimeError(f"generated {profile} README is not editor-first")
131
+ required_guidance = [
132
+ f"**`{profile}`** profile: {description}",
133
+ f"## Editable Content For `{profile}`",
134
+ profile_path,
135
+ "only one active editor per file",
136
+ "Never edit or commit directly to `main`",
137
+ "Draft pull request",
138
+ "Starts the deploy workflow manually",
139
+ ]
140
+ missing_guidance = [value for value in required_guidance if value not in readme]
141
+ if missing_guidance:
142
+ raise RuntimeError(f"generated {profile} README is missing guidance: {missing_guidance}")
143
+ if f"Wheel {profile}" in readme:
144
+ raise RuntimeError(f"generated {profile} README interpolated the user-controlled title")
145
+ if any(f"`{known_profile}`" not in readme for known_profile in profiles):
146
+ raise RuntimeError(f"generated {profile} README does not explain all profiles")
147
+ if "correctly named short-lived branch" not in pr_template or "Required local checks and renders pass" not in pr_template:
148
+ raise RuntimeError(f"generated {profile} pull request template lacks coordination checks")
149
+ collaboration_markers = [
150
+ "content/<issue>-<slug>",
151
+ "reference/<issue>-<slug>",
152
+ "figure/<provider>/<issue>-<slug>",
153
+ "fix/<issue>-<slug>",
154
+ "feat/<issue>-<slug>",
155
+ "docs/<issue>-<slug>",
156
+ "chore/<issue>-<slug>",
157
+ "format/<issue>-<slug>",
158
+ "integration/<provider>/<issue>-<slug>",
159
+ "A maintainer must accept an exact reservation",
160
+ "not broad directories or globs",
161
+ "dedicated Git worktree",
162
+ "Never share a mutable checkout",
163
+ "one atomic bundle",
164
+ "source, every local data/input file, output, caption-bearing content reference",
165
+ "immutable release, full commit SHA, or image digest",
166
+ "Draft pull request after the first coherent change",
167
+ "delete the local and remote task branch",
168
+ "remove the worktree",
169
+ ]
170
+ if any(value not in collaboration for value in collaboration_markers):
171
+ raise RuntimeError(f"generated {profile} collaboration contract is incomplete")
172
+ if "dependency-name: unaltraweb" not in dependabot or f"dependency-name: {integration['site_deploy_workflow']}" not in dependabot:
173
+ raise RuntimeError(f"generated {profile} Dependabot policy permits partial integration updates")
174
+ combined_guidance = readme + collaboration + pr_template
175
+ forbidden = ["${{", "secrets.", "GITHUB_TOKEN", "id-token:", "contents: write", "on:\n push:"]
176
+ if any(value in combined_guidance for value in forbidden):
177
+ raise RuntimeError(f"generated {profile} editor guidance exposes workflow or secret internals")
178
+ auto_publish = ["publishes automatically", "deploys automatically", "automatic deployment"]
179
+ if any(value in combined_guidance.lower() for value in auto_publish):
180
+ raise RuntimeError(f"generated {profile} editor guidance implies automatic publication")
181
+ if profile == "unaltremanual":
182
+ manual_markers = [
183
+ "`latest` is a manual-only deployment",
184
+ "Pushing or merging to `main` does not publish the manual",
185
+ "are generated for deployment and are not versioned",
186
+ "`vYYYY.MM(.N)`",
187
+ "Release checks reject `legacy/` or `sandbox/`",
188
+ ]
189
+ if any(value not in readme for value in manual_markers):
190
+ raise RuntimeError("generated unaltremanual README lacks release-channel guidance")
191
+ elif "## unaltremanual Publishing Channels" in readme:
192
+ raise RuntimeError(f"generated {profile} README contains unaltremanual-only release guidance")
193
+ ignore_lines = (profile_site / ".gitignore").read_text(encoding="utf-8").splitlines()
194
+ expects_manual_outputs = profile == "unaltremanual"
195
+ if ("/assets/pdf/manual-en.pdf" in ignore_lines) != expects_manual_outputs or ("/assets/img/manual-cover-en.png" in ignore_lines) != expects_manual_outputs:
196
+ raise RuntimeError(f"generated {profile} .gitignore has incorrect manual output scope")
197
+ if "*.pdf" in ignore_lines or "*.png" in ignore_lines:
198
+ raise RuntimeError(f"generated {profile} .gitignore ignores arbitrary publication assets")
199
+ gemfile = (profile_site / "Gemfile").read_text(encoding="utf-8")
200
+ lockfile = (profile_site / "Gemfile.lock").read_text(encoding="utf-8")
201
+ deploy = (profile_site / ".github/workflows/deploy.yml").read_text(encoding="utf-8")
202
+ if f'git: "{integration["core_repository"]}"' not in gemfile or f'ref: "{integration["core_sha"]}"' not in gemfile:
203
+ raise RuntimeError(f"generated {profile} Gemfile does not use the reviewed core revision")
204
+ if f'revision: {integration["core_sha"]}' not in lockfile:
205
+ raise RuntimeError(f"generated {profile} lockfile does not use the reviewed core revision")
206
+ if f'uses: {integration["site_deploy_workflow"]}@{integration["core_sha"]}' not in deploy:
207
+ raise RuntimeError(f"generated {profile} deploy caller does not use the reviewed core revision")
208
+ if f'manual-pdf-image: "{integration["manual_pdf_image"]}"' not in deploy:
209
+ raise RuntimeError(f"generated {profile} deploy caller does not use the reviewed PDF image")
210
+ if f'vegavisuals-sha: "{integration["vegavisuals_sha"]}"' not in deploy:
211
+ raise RuntimeError(f"generated {profile} deploy caller does not use the reviewed Vega renderer")
212
+ manifest = json.loads((profile_site / ".unaltraweb/scaffold.json").read_text(encoding="utf-8"))
213
+ expected_managed = {
214
+ ".gitignore",
215
+ ".unaltraweb/docker-mount.sh",
216
+ ".github/CONTRIBUTING.md",
217
+ ".github/dependabot.yml",
218
+ "Makefile",
219
+ "Gemfile",
220
+ "Gemfile.lock",
221
+ ".github/pull_request_template.md",
222
+ ".github/workflows/deploy.yml",
223
+ }
224
+ if set(manifest["files"]) != expected_managed:
225
+ raise RuntimeError(f"generated {profile} scaffold baseline is incomplete: {manifest['files']}")
226
+ sites[profile] = profile_site
227
+
228
+ site = sites["unaltredocs"]
229
+ if not (site / ".unaltraweb/scaffold.json").is_file():
230
+ raise RuntimeError("new-web did not install the package scaffold baseline")
231
+
232
+ manual_site = temp / "manual-site"
233
+ manual_created = json.loads(run([str(cli), "--project", str(manual_site), "new-web", "--site-profile", "unaltremanual"], cwd=temp).stdout)
234
+ if not manual_created["ok"]:
235
+ raise RuntimeError(f"manual new-web failed from clean wheel: {manual_created}")
236
+ for required_path in [
237
+ "AGENTS.md",
238
+ "README.md",
239
+ "_pages/index.html",
240
+ "_chapters/en/.gitkeep",
241
+ "assets/quarto/.gitkeep",
242
+ "assets/img/generated/.gitkeep",
243
+ ]:
244
+ if not (manual_site / required_path).is_file():
245
+ raise RuntimeError(f"manual new-web did not create {required_path}")
246
+ computation_config = (manual_site / ".unaltraweb/computations.yml").read_text(encoding="utf-8")
247
+ for component_id in ["compute_python", "compute_r"]:
248
+ if contract["components"][component_id]["reference"] not in computation_config:
249
+ raise RuntimeError(f"new-web did not select the {component_id} worker from the component contract")
250
+ manual_doctor = json.loads(run([str(cli), "doctor", "--project", str(manual_site)], cwd=temp).stdout)
251
+ if not {"compute_python", "compute_r"}.issubset(manual_doctor["selected_components"]):
252
+ raise RuntimeError(f"manual project doctor did not select both computation workers: {manual_doctor}")
253
+ detected = json.loads(run([str(cli), "--project", str(site), "mcp", "detect-site"], cwd=temp).stdout)
254
+ if not detected["is_unaltraweb_site"]:
255
+ raise RuntimeError(f"package-only inspection failed from clean wheel: {detected}")
256
+ project_doctor = json.loads(run([str(cli), "doctor", "--project", str(site)], cwd=temp).stdout)
257
+ if not project_doctor["ok"] or project_doctor["project"]["profile"] != "unaltredocs":
258
+ raise RuntimeError(f"project doctor failed from clean wheel: {project_doctor}")
259
+ site_doctor = json.loads(run([str(cli), "--project", str(site), "mcp", "site-doctor"], cwd=temp).stdout)
260
+ if not site_doctor["ok"] or not site_doctor["offline"]:
261
+ raise RuntimeError(f"site doctor failed from clean wheel: {site_doctor}")
262
+ source = json.loads(run([str(cli), "--project", str(site), "mcp", "site-source-read", "--path", "_pages/en/index.md"], cwd=temp).stdout)
263
+ source_dry_run = json.loads(run([
264
+ str(cli), "--project", str(site), "mcp", "site-source-write",
265
+ "--path", "_pages/en/index.md", "--content", source["content"] + "\nWheel source test.\n",
266
+ "--expected-sha256", source["sha256"],
267
+ ], cwd=temp).stdout)
268
+ if not source_dry_run["dry_run"] or (site / "_pages/en/index.md").read_text(encoding="utf-8") != source["content"]:
269
+ raise RuntimeError(f"wheel source dry-run mutated content: {source_dry_run}")
270
+ scaffold = json.loads(run([str(cli), "--project", str(site), "mcp", "scaffold-sync"], cwd=temp).stdout)
271
+ if not scaffold["ok"] or not scaffold["dry_run"]:
272
+ raise RuntimeError(f"wheel scaffold sync dry-run failed: {scaffold}")
273
+
274
+ factory_error = run([str(cli), "--project", str(site), "mcp", "prompts"], cwd=temp, expected=1)
275
+ if "requires the unaltraweb factory checkout" not in factory_error.stderr:
276
+ raise RuntimeError(f"Factory-required command did not fail clearly: {factory_error.stderr}")
277
+ print(json.dumps({"ok": True, "version": version, "wheel": wheels[0].name, "mode": doctor["mode"]}, sort_keys=True))
278
+ return 0
279
+
280
+
281
+ if __name__ == "__main__":
282
+ raise SystemExit(main())
@@ -0,0 +1,36 @@
1
+ #!/bin/sh
2
+ set -eu
3
+
4
+ if [ "$#" -lt 2 ] || [ "$#" -gt 3 ]; then
5
+ printf '%s\n' 'Usage: unaltraweb-docker-mount SOURCE TARGET [readonly]' >&2
6
+ exit 2
7
+ fi
8
+ case "$1" in
9
+ /*) ;;
10
+ *) printf '%s\n' 'Docker bind source must be an absolute path.' >&2; exit 2 ;;
11
+ esac
12
+ case "$2" in
13
+ /*) ;;
14
+ *) printf '%s\n' 'Docker bind target must be an absolute path.' >&2; exit 2 ;;
15
+ esac
16
+ if [ "$#" -eq 3 ] && [ "$3" != "readonly" ]; then
17
+ printf '%s\n' 'The optional Docker bind mode must be readonly.' >&2
18
+ exit 2
19
+ fi
20
+ newline='
21
+ '
22
+ carriage_return=$(printf '\r')
23
+ case "$1$2" in
24
+ *"$newline"*|*"$carriage_return"*)
25
+ printf '%s\n' 'Docker bind source and target must not contain carriage returns or newlines.' >&2
26
+ exit 2
27
+ ;;
28
+ esac
29
+
30
+ source_field=$(printf 'source=%s' "$1" | sed 's/"/""/g')
31
+ target_field=$(printf 'target=%s' "$2" | sed 's/"/""/g')
32
+ printf 'type=bind,"%s","%s"' "$source_field" "$target_field"
33
+ if [ "$#" -eq 3 ]; then
34
+ printf ',readonly'
35
+ fi
36
+ printf '\n'