dsh-aris-panel 0.1.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 (442) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +98 -0
  3. package/README_CN.md +87 -0
  4. package/dsh/checkout.patch.yml +38 -0
  5. package/dsh/client.js +634 -0
  6. package/dsh/cordis.patch.yml +44 -0
  7. package/dsh/index.mjs +76 -0
  8. package/dsh/run-status.mjs +182 -0
  9. package/dsh/scope-limits.mjs +50 -0
  10. package/dsh/workbench.mjs +291 -0
  11. package/mcp-servers/claude-review/README.md +93 -0
  12. package/mcp-servers/claude-review/run_with_claude_aws.sh +49 -0
  13. package/mcp-servers/claude-review/server.py +718 -0
  14. package/mcp-servers/codex-image2/README.md +65 -0
  15. package/mcp-servers/codex-image2/server.py +893 -0
  16. package/mcp-servers/feishu-bridge/requirements.txt +1 -0
  17. package/mcp-servers/feishu-bridge/server.py +240 -0
  18. package/mcp-servers/gemini-review/README.md +171 -0
  19. package/mcp-servers/gemini-review/server.py +1856 -0
  20. package/mcp-servers/llm-chat/requirements.txt +1 -0
  21. package/mcp-servers/llm-chat/server.py +664 -0
  22. package/mcp-servers/manual-review/README.md +133 -0
  23. package/mcp-servers/manual-review/server.py +910 -0
  24. package/mcp-servers/manual-review/ui.html +279 -0
  25. package/mcp-servers/minimax-chat/requirements.txt +1 -0
  26. package/mcp-servers/minimax-chat/server.py +381 -0
  27. package/package.json +51 -0
  28. package/skills/ablation-planner/SKILL.md +123 -0
  29. package/skills/alphaxiv/SKILL.md +196 -0
  30. package/skills/analyze-results/SKILL.md +46 -0
  31. package/skills/arxiv/SKILL.md +248 -0
  32. package/skills/auto-paper-improvement-loop/SKILL.md +651 -0
  33. package/skills/auto-review-loop/SKILL.md +1137 -0
  34. package/skills/auto-review-loop-llm/SKILL.md +259 -0
  35. package/skills/auto-review-loop-minimax/SKILL.md +302 -0
  36. package/skills/citation-audit/SKILL.md +502 -0
  37. package/skills/claims-drafting/SKILL.md +227 -0
  38. package/skills/comm-lit-review/SKILL.md +297 -0
  39. package/skills/deepxiv/SKILL.md +263 -0
  40. package/skills/dse-loop/SKILL.md +296 -0
  41. package/skills/embodiment-description/SKILL.md +129 -0
  42. package/skills/exa-search/SKILL.md +205 -0
  43. package/skills/experiment-audit/SKILL.md +311 -0
  44. package/skills/experiment-bridge/SKILL.md +376 -0
  45. package/skills/experiment-plan/SKILL.md +249 -0
  46. package/skills/experiment-queue/SKILL.md +431 -0
  47. package/skills/experiment-queue/scripts/build_manifest.py +142 -0
  48. package/skills/experiment-queue/scripts/queue_manager.py +433 -0
  49. package/skills/feishu-notify/SKILL.md +156 -0
  50. package/skills/figure-description/SKILL.md +138 -0
  51. package/skills/figure-spec/SKILL.md +262 -0
  52. package/skills/figure-spec/scripts/figure_renderer.py +799 -0
  53. package/skills/formula-derivation/SKILL.md +280 -0
  54. package/skills/gemini-search/SKILL.md +231 -0
  55. package/skills/grant-proposal/SKILL.md +698 -0
  56. package/skills/idea-creator/SKILL.md +542 -0
  57. package/skills/idea-discovery/SKILL.md +521 -0
  58. package/skills/idea-discovery-robot/SKILL.md +363 -0
  59. package/skills/integrity-forensics/SKILL.md +284 -0
  60. package/skills/interview-cheatsheet/SKILL.md +245 -0
  61. package/skills/invention-structuring/SKILL.md +188 -0
  62. package/skills/jurisdiction-format/SKILL.md +192 -0
  63. package/skills/kill-argument/SKILL.md +437 -0
  64. package/skills/mermaid-diagram/SKILL.md +419 -0
  65. package/skills/meta-apply/SKILL.md +141 -0
  66. package/skills/meta-optimize/SKILL.md +437 -0
  67. package/skills/monitor-experiment/SKILL.md +140 -0
  68. package/skills/novelty-check/SKILL.md +101 -0
  69. package/skills/openalex/SKILL.md +237 -0
  70. package/skills/overleaf-sync/SKILL.md +220 -0
  71. package/skills/paper-claim-audit/SKILL.md +348 -0
  72. package/skills/paper-compile/SKILL.md +266 -0
  73. package/skills/paper-figure/SKILL.md +312 -0
  74. package/skills/paper-illustration/SKILL.md +736 -0
  75. package/skills/paper-illustration-image2/SKILL.md +391 -0
  76. package/skills/paper-illustration-image2/scripts/paper_illustration_image2.py +255 -0
  77. package/skills/paper-plan/SKILL.md +386 -0
  78. package/skills/paper-poster/SKILL.md +19 -0
  79. package/skills/paper-poster-html/DESIGN_FINAL.md +176 -0
  80. package/skills/paper-poster-html/IMPLEMENTATION_CONVENTIONS.md +161 -0
  81. package/skills/paper-poster-html/LICENSES/posterly-MIT.txt +21 -0
  82. package/skills/paper-poster-html/NOTICE.md +57 -0
  83. package/skills/paper-poster-html/SKILL.md +323 -0
  84. package/skills/paper-poster-html/scripts/_posterly/__init__.py +0 -0
  85. package/skills/paper-poster-html/scripts/_posterly/canvas.py +200 -0
  86. package/skills/paper-poster-html/scripts/_posterly/measure.py +588 -0
  87. package/skills/paper-poster-html/scripts/_posterly/polish.py +498 -0
  88. package/skills/paper-poster-html/scripts/_posterly/preflight.py +489 -0
  89. package/skills/paper-poster-html/scripts/_posterly/render.py +215 -0
  90. package/skills/paper-poster-html/scripts/_posterly/textutil.py +16 -0
  91. package/skills/paper-poster-html/scripts/_posterly/verify_final.py +171 -0
  92. package/skills/paper-poster-html/scripts/asset_check.py +897 -0
  93. package/skills/paper-poster-html/scripts/extract_pdf_figures.py +666 -0
  94. package/skills/paper-poster-html/scripts/poster_check.py +251 -0
  95. package/skills/paper-poster-html/scripts/preprocess_figures.py +238 -0
  96. package/skills/paper-poster-html/scripts/render_preview.py +217 -0
  97. package/skills/paper-poster-html/scripts/run_gates.py +556 -0
  98. package/skills/paper-poster-html/scripts/style_check.py +1324 -0
  99. package/skills/paper-poster-html/templates/COMPONENTS.md +462 -0
  100. package/skills/paper-poster-html/templates/README.md +170 -0
  101. package/skills/paper-poster-html/templates/landscape_4col.html +1032 -0
  102. package/skills/paper-poster-html/templates/landscape_hero.html +1046 -0
  103. package/skills/paper-poster-html/templates/portrait_2col.html +947 -0
  104. package/skills/paper-poster-html/templates/tokens/acl.json +9 -0
  105. package/skills/paper-poster-html/templates/tokens/cvpr.json +9 -0
  106. package/skills/paper-poster-html/templates/tokens/generic.json +9 -0
  107. package/skills/paper-poster-html/templates/tokens/iclr.json +9 -0
  108. package/skills/paper-poster-html/templates/tokens/icml.json +9 -0
  109. package/skills/paper-poster-html/templates/tokens/neurips.json +9 -0
  110. package/skills/paper-slides/SKILL.md +635 -0
  111. package/skills/paper-talk/SKILL.md +381 -0
  112. package/skills/paper-write/SKILL.md +604 -0
  113. package/skills/paper-write/templates/IEEEtran.bst +2409 -0
  114. package/skills/paper-write/templates/IEEEtran.cls +6347 -0
  115. package/skills/paper-write/templates/iclr2026.tex +84 -0
  116. package/skills/paper-write/templates/icml2025.tex +87 -0
  117. package/skills/paper-write/templates/ieee_conference.tex +89 -0
  118. package/skills/paper-write/templates/ieee_journal.tex +93 -0
  119. package/skills/paper-write/templates/math_commands.tex +48 -0
  120. package/skills/paper-write/templates/neurips2025.tex +80 -0
  121. package/skills/paper-writing/SKILL.md +916 -0
  122. package/skills/patent-novelty-check/SKILL.md +153 -0
  123. package/skills/patent-pipeline/SKILL.md +344 -0
  124. package/skills/patent-review/SKILL.md +203 -0
  125. package/skills/pixel-art/SKILL.md +137 -0
  126. package/skills/prior-art-search/SKILL.md +146 -0
  127. package/skills/proof-checker/SKILL.md +866 -0
  128. package/skills/proof-orchestrator/NOTICE.md +24 -0
  129. package/skills/proof-orchestrator/SKILL.md +254 -0
  130. package/skills/proof-orchestrator/references/audit-output-contract.md +126 -0
  131. package/skills/proof-orchestrator/references/deepseek-routing.md +74 -0
  132. package/skills/proof-orchestrator/references/dispatch-prompts.md +227 -0
  133. package/skills/proof-orchestrator/references/notation-audit.md +135 -0
  134. package/skills/proof-orchestrator/references/proof-audit-rubric.md +70 -0
  135. package/skills/proof-orchestrator/references/stress-tests.md +38 -0
  136. package/skills/proof-writer/SKILL.md +223 -0
  137. package/skills/qzcli/SKILL.md +324 -0
  138. package/skills/rebuttal/SKILL.md +376 -0
  139. package/skills/render-html/SKILL.md +316 -0
  140. package/skills/render-html/scripts/render_html.py +1006 -0
  141. package/skills/render-html/scripts/templates/academic.html +703 -0
  142. package/skills/render-html/scripts/templates/dashboard.html +333 -0
  143. package/skills/research-lit/SKILL.md +756 -0
  144. package/skills/research-pipeline/SKILL.md +384 -0
  145. package/skills/research-refine/SKILL.md +770 -0
  146. package/skills/research-refine-pipeline/SKILL.md +186 -0
  147. package/skills/research-review/SKILL.md +198 -0
  148. package/skills/research-wiki/SKILL.md +461 -0
  149. package/skills/resubmit-pipeline/SKILL.md +447 -0
  150. package/skills/result-to-claim/SKILL.md +311 -0
  151. package/skills/run-experiment/SKILL.md +313 -0
  152. package/skills/semantic-scholar/SKILL.md +236 -0
  153. package/skills/serverless-modal/SKILL.md +335 -0
  154. package/skills/shared-references/acceptance-gate.md +324 -0
  155. package/skills/shared-references/assurance-contract.md +248 -0
  156. package/skills/shared-references/capture-antipatterns.md +78 -0
  157. package/skills/shared-references/citation-discipline.md +583 -0
  158. package/skills/shared-references/compute-env-contract.md +163 -0
  159. package/skills/shared-references/effort-contract.md +183 -0
  160. package/skills/shared-references/evidence-precheck.md +65 -0
  161. package/skills/shared-references/experiment-integrity.md +49 -0
  162. package/skills/shared-references/external-cadence.md +326 -0
  163. package/skills/shared-references/fan-out-pattern.md +366 -0
  164. package/skills/shared-references/injection-hygiene.md +127 -0
  165. package/skills/shared-references/integration-contract.md +461 -0
  166. package/skills/shared-references/output-composition.md +93 -0
  167. package/skills/shared-references/output-language.md +45 -0
  168. package/skills/shared-references/output-manifest.md +49 -0
  169. package/skills/shared-references/output-versioning.md +111 -0
  170. package/skills/shared-references/patent-format-cn.md +199 -0
  171. package/skills/shared-references/patent-format-ep.md +173 -0
  172. package/skills/shared-references/patent-format-us.md +161 -0
  173. package/skills/shared-references/patent-writing-principles.md +197 -0
  174. package/skills/shared-references/prior-art-databases.md +141 -0
  175. package/skills/shared-references/resumable-runs.md +109 -0
  176. package/skills/shared-references/review-scope-limits.md +81 -0
  177. package/skills/shared-references/review-tracing.md +391 -0
  178. package/skills/shared-references/reviewer-independence.md +79 -0
  179. package/skills/shared-references/reviewer-routing.md +852 -0
  180. package/skills/shared-references/skill-governance.md +104 -0
  181. package/skills/shared-references/taste-calibration.md +85 -0
  182. package/skills/shared-references/venue-checklists.md +114 -0
  183. package/skills/shared-references/wiki-helper-resolution.md +134 -0
  184. package/skills/shared-references/writing-principles.md +525 -0
  185. package/skills/skills-codex/README.md +102 -0
  186. package/skills/skills-codex/README_CN.md +100 -0
  187. package/skills/skills-codex/ablation-planner/SKILL.md +126 -0
  188. package/skills/skills-codex/alphaxiv/SKILL.md +186 -0
  189. package/skills/skills-codex/analyze-results/SKILL.md +45 -0
  190. package/skills/skills-codex/arxiv/SKILL.md +210 -0
  191. package/skills/skills-codex/auto-paper-improvement-loop/SKILL.md +574 -0
  192. package/skills/skills-codex/auto-review-loop/SKILL.md +500 -0
  193. package/skills/skills-codex/auto-review-loop-llm/SKILL.md +247 -0
  194. package/skills/skills-codex/auto-review-loop-minimax/SKILL.md +290 -0
  195. package/skills/skills-codex/citation-audit/SKILL.md +504 -0
  196. package/skills/skills-codex/claims-drafting/SKILL.md +239 -0
  197. package/skills/skills-codex/comm-lit-review/SKILL.md +299 -0
  198. package/skills/skills-codex/comm-lit-review/references/domain-taxonomy.md +57 -0
  199. package/skills/skills-codex/comm-lit-review/references/output-template.md +37 -0
  200. package/skills/skills-codex/comm-lit-review/references/source-policy.md +99 -0
  201. package/skills/skills-codex/comm-lit-review/references/venue-tiering.md +112 -0
  202. package/skills/skills-codex/deepxiv/SKILL.md +142 -0
  203. package/skills/skills-codex/dse-loop/SKILL.md +285 -0
  204. package/skills/skills-codex/embodiment-description/SKILL.md +129 -0
  205. package/skills/skills-codex/exa-search/SKILL.md +192 -0
  206. package/skills/skills-codex/experiment-audit/SKILL.md +286 -0
  207. package/skills/skills-codex/experiment-bridge/SKILL.md +356 -0
  208. package/skills/skills-codex/experiment-plan/SKILL.md +249 -0
  209. package/skills/skills-codex/experiment-queue/SKILL.md +401 -0
  210. package/skills/skills-codex/feishu-notify/SKILL.md +155 -0
  211. package/skills/skills-codex/figure-description/SKILL.md +138 -0
  212. package/skills/skills-codex/figure-spec/SKILL.md +252 -0
  213. package/skills/skills-codex/formula-derivation/SKILL.md +280 -0
  214. package/skills/skills-codex/gemini-search/SKILL.md +205 -0
  215. package/skills/skills-codex/grant-proposal/SKILL.md +626 -0
  216. package/skills/skills-codex/idea-creator/SKILL.md +405 -0
  217. package/skills/skills-codex/idea-discovery/SKILL.md +475 -0
  218. package/skills/skills-codex/idea-discovery-robot/SKILL.md +362 -0
  219. package/skills/skills-codex/integrity-forensics/SKILL.md +106 -0
  220. package/skills/skills-codex/interview-cheatsheet/SKILL.md +245 -0
  221. package/skills/skills-codex/invention-structuring/SKILL.md +188 -0
  222. package/skills/skills-codex/jurisdiction-format/SKILL.md +192 -0
  223. package/skills/skills-codex/kill-argument/SKILL.md +403 -0
  224. package/skills/skills-codex/mermaid-diagram/SKILL.md +379 -0
  225. package/skills/skills-codex/meta-apply/SKILL.md +154 -0
  226. package/skills/skills-codex/meta-optimize/SKILL.md +348 -0
  227. package/skills/skills-codex/monitor-experiment/SKILL.md +98 -0
  228. package/skills/skills-codex/novelty-check/SKILL.md +89 -0
  229. package/skills/skills-codex/openalex/SKILL.md +228 -0
  230. package/skills/skills-codex/overleaf-sync/SKILL.md +220 -0
  231. package/skills/skills-codex/paper-claim-audit/SKILL.md +350 -0
  232. package/skills/skills-codex/paper-compile/SKILL.md +253 -0
  233. package/skills/skills-codex/paper-figure/SKILL.md +311 -0
  234. package/skills/skills-codex/paper-illustration/SKILL.md +690 -0
  235. package/skills/skills-codex/paper-illustration-image2/SKILL.md +383 -0
  236. package/skills/skills-codex/paper-illustration-image2/scripts/paper_illustration_image2.py +255 -0
  237. package/skills/skills-codex/paper-plan/SKILL.md +278 -0
  238. package/skills/skills-codex/paper-poster/SKILL.md +19 -0
  239. package/skills/skills-codex/paper-poster-html/SKILL.md +377 -0
  240. package/skills/skills-codex/paper-slides/SKILL.md +571 -0
  241. package/skills/skills-codex/paper-talk/SKILL.md +381 -0
  242. package/skills/skills-codex/paper-write/SKILL.md +411 -0
  243. package/skills/skills-codex/paper-write/templates/IEEEtran.bst +2409 -0
  244. package/skills/skills-codex/paper-write/templates/IEEEtran.cls +6347 -0
  245. package/skills/skills-codex/paper-write/templates/aaai2026.bst +1493 -0
  246. package/skills/skills-codex/paper-write/templates/aaai2026.sty +315 -0
  247. package/skills/skills-codex/paper-write/templates/aaai2026.tex +952 -0
  248. package/skills/skills-codex/paper-write/templates/acl.sty +312 -0
  249. package/skills/skills-codex/paper-write/templates/acl2026.tex +377 -0
  250. package/skills/skills-codex/paper-write/templates/acl_natbib.bst +1940 -0
  251. package/skills/skills-codex/paper-write/templates/acm.bst +3081 -0
  252. package/skills/skills-codex/paper-write/templates/acm_mm2026.tex +204 -0
  253. package/skills/skills-codex/paper-write/templates/acmart.cls +3520 -0
  254. package/skills/skills-codex/paper-write/templates/cvpr.bst +1448 -0
  255. package/skills/skills-codex/paper-write/templates/cvpr.sty +508 -0
  256. package/skills/skills-codex/paper-write/templates/cvpr2026.tex +63 -0
  257. package/skills/skills-codex/paper-write/templates/iclr2026.tex +84 -0
  258. package/skills/skills-codex/paper-write/templates/iclr2026_conference.bst +1440 -0
  259. package/skills/skills-codex/paper-write/templates/iclr2026_conference.sty +246 -0
  260. package/skills/skills-codex/paper-write/templates/icml2026.sty +767 -0
  261. package/skills/skills-codex/paper-write/templates/icml2026.tex +662 -0
  262. package/skills/skills-codex/paper-write/templates/ieee_conference.tex +89 -0
  263. package/skills/skills-codex/paper-write/templates/ieee_journal.tex +93 -0
  264. package/skills/skills-codex/paper-write/templates/math_commands.tex +48 -0
  265. package/skills/skills-codex/paper-write/templates/neurips2026.tex +493 -0
  266. package/skills/skills-codex/paper-write/templates/neurips_2026.sty +437 -0
  267. package/skills/skills-codex/paper-writing/SKILL.md +731 -0
  268. package/skills/skills-codex/patent-novelty-check/SKILL.md +153 -0
  269. package/skills/skills-codex/patent-pipeline/SKILL.md +344 -0
  270. package/skills/skills-codex/patent-review/SKILL.md +202 -0
  271. package/skills/skills-codex/pixel-art/SKILL.md +139 -0
  272. package/skills/skills-codex/prior-art-search/SKILL.md +146 -0
  273. package/skills/skills-codex/proof-checker/SKILL.md +554 -0
  274. package/skills/skills-codex/proof-orchestrator/SKILL.md +260 -0
  275. package/skills/skills-codex/proof-orchestrator/references/audit-output-contract.md +126 -0
  276. package/skills/skills-codex/proof-orchestrator/references/deepseek-routing.md +76 -0
  277. package/skills/skills-codex/proof-orchestrator/references/dispatch-prompts.md +227 -0
  278. package/skills/skills-codex/proof-orchestrator/references/notation-audit.md +135 -0
  279. package/skills/skills-codex/proof-orchestrator/references/proof-audit-rubric.md +70 -0
  280. package/skills/skills-codex/proof-orchestrator/references/stress-tests.md +38 -0
  281. package/skills/skills-codex/proof-writer/SKILL.md +222 -0
  282. package/skills/skills-codex/qzcli/SKILL.md +324 -0
  283. package/skills/skills-codex/rebuttal/SKILL.md +305 -0
  284. package/skills/skills-codex/render-html/SKILL.md +305 -0
  285. package/skills/skills-codex/render-html/scripts/__pycache__/render_html.cpython-314.pyc +0 -0
  286. package/skills/skills-codex/render-html/scripts/render_html.py +909 -0
  287. package/skills/skills-codex/render-html/scripts/templates/academic.html +342 -0
  288. package/skills/skills-codex/render-html/scripts/templates/dashboard.html +333 -0
  289. package/skills/skills-codex/research-lit/SKILL.md +464 -0
  290. package/skills/skills-codex/research-pipeline/SKILL.md +340 -0
  291. package/skills/skills-codex/research-refine/SKILL.md +721 -0
  292. package/skills/skills-codex/research-refine-pipeline/SKILL.md +186 -0
  293. package/skills/skills-codex/research-review/SKILL.md +135 -0
  294. package/skills/skills-codex/research-wiki/SKILL.md +421 -0
  295. package/skills/skills-codex/resubmit-pipeline/SKILL.md +444 -0
  296. package/skills/skills-codex/result-to-claim/SKILL.md +246 -0
  297. package/skills/skills-codex/run-experiment/SKILL.md +236 -0
  298. package/skills/skills-codex/semantic-scholar/SKILL.md +219 -0
  299. package/skills/skills-codex/serverless-modal/SKILL.md +335 -0
  300. package/skills/skills-codex/shared-references/acceptance-gate.md +336 -0
  301. package/skills/skills-codex/shared-references/assurance-contract.md +139 -0
  302. package/skills/skills-codex/shared-references/capture-antipatterns.md +84 -0
  303. package/skills/skills-codex/shared-references/citation-discipline.md +452 -0
  304. package/skills/skills-codex/shared-references/compute-env-contract.md +163 -0
  305. package/skills/skills-codex/shared-references/effort-contract.md +143 -0
  306. package/skills/skills-codex/shared-references/evidence-precheck.md +73 -0
  307. package/skills/skills-codex/shared-references/experiment-integrity.md +49 -0
  308. package/skills/skills-codex/shared-references/external-cadence.md +334 -0
  309. package/skills/skills-codex/shared-references/fan-out-pattern.md +375 -0
  310. package/skills/skills-codex/shared-references/injection-hygiene.md +132 -0
  311. package/skills/skills-codex/shared-references/integration-contract.md +372 -0
  312. package/skills/skills-codex/shared-references/output-composition.md +98 -0
  313. package/skills/skills-codex/shared-references/output-language.md +45 -0
  314. package/skills/skills-codex/shared-references/output-manifest.md +40 -0
  315. package/skills/skills-codex/shared-references/output-versioning.md +111 -0
  316. package/skills/skills-codex/shared-references/patent-format-cn.md +199 -0
  317. package/skills/skills-codex/shared-references/patent-format-ep.md +173 -0
  318. package/skills/skills-codex/shared-references/patent-format-us.md +161 -0
  319. package/skills/skills-codex/shared-references/patent-writing-principles.md +197 -0
  320. package/skills/skills-codex/shared-references/prior-art-databases.md +141 -0
  321. package/skills/skills-codex/shared-references/resumable-runs.md +125 -0
  322. package/skills/skills-codex/shared-references/review-scope-limits.md +81 -0
  323. package/skills/skills-codex/shared-references/review-tracing.md +144 -0
  324. package/skills/skills-codex/shared-references/reviewer-independence.md +66 -0
  325. package/skills/skills-codex/shared-references/reviewer-routing.md +128 -0
  326. package/skills/skills-codex/shared-references/skill-governance.md +119 -0
  327. package/skills/skills-codex/shared-references/taste-calibration.md +90 -0
  328. package/skills/skills-codex/shared-references/venue-checklists.md +73 -0
  329. package/skills/skills-codex/shared-references/wiki-helper-resolution.md +69 -0
  330. package/skills/skills-codex/shared-references/writing-principles.md +525 -0
  331. package/skills/skills-codex/slides-polish/SKILL.md +563 -0
  332. package/skills/skills-codex/specification-writing/SKILL.md +211 -0
  333. package/skills/skills-codex/system-profile/SKILL.md +103 -0
  334. package/skills/skills-codex/training-check/SKILL.md +83 -0
  335. package/skills/skills-codex/vast-gpu/SKILL.md +394 -0
  336. package/skills/skills-codex/web-debug-search/SKILL.md +334 -0
  337. package/skills/skills-codex/wiki-enrich/SKILL.md +255 -0
  338. package/skills/skills-codex/writing-systems-papers/SKILL.md +184 -0
  339. package/skills/skills-codex-claude-review/README.md +79 -0
  340. package/skills/skills-codex-claude-review/README_CN.md +78 -0
  341. package/skills/skills-codex-claude-review/auto-paper-improvement-loop/SKILL.md +581 -0
  342. package/skills/skills-codex-claude-review/auto-review-loop/SKILL.md +510 -0
  343. package/skills/skills-codex-claude-review/novelty-check/SKILL.md +102 -0
  344. package/skills/skills-codex-claude-review/paper-figure/SKILL.md +319 -0
  345. package/skills/skills-codex-claude-review/paper-plan/SKILL.md +287 -0
  346. package/skills/skills-codex-claude-review/paper-write/SKILL.md +420 -0
  347. package/skills/skills-codex-claude-review/research-refine/SKILL.md +732 -0
  348. package/skills/skills-codex-claude-review/research-review/SKILL.md +149 -0
  349. package/skills/skills-codex-gemini-review/README.md +176 -0
  350. package/skills/skills-codex-gemini-review/README_CN.md +175 -0
  351. package/skills/skills-codex-gemini-review/auto-paper-improvement-loop/SKILL.md +331 -0
  352. package/skills/skills-codex-gemini-review/auto-review-loop/SKILL.md +304 -0
  353. package/skills/skills-codex-gemini-review/grant-proposal/SKILL.md +630 -0
  354. package/skills/skills-codex-gemini-review/idea-creator/SKILL.md +263 -0
  355. package/skills/skills-codex-gemini-review/idea-discovery/SKILL.md +275 -0
  356. package/skills/skills-codex-gemini-review/idea-discovery-robot/SKILL.md +365 -0
  357. package/skills/skills-codex-gemini-review/novelty-check/SKILL.md +92 -0
  358. package/skills/skills-codex-gemini-review/paper-figure/SKILL.md +289 -0
  359. package/skills/skills-codex-gemini-review/paper-plan/SKILL.md +265 -0
  360. package/skills/skills-codex-gemini-review/paper-poster-html/SKILL.md +102 -0
  361. package/skills/skills-codex-gemini-review/paper-slides/SKILL.md +582 -0
  362. package/skills/skills-codex-gemini-review/paper-write/SKILL.md +346 -0
  363. package/skills/skills-codex-gemini-review/paper-writing/SKILL.md +312 -0
  364. package/skills/skills-codex-gemini-review/research-refine/SKILL.md +674 -0
  365. package/skills/skills-codex-gemini-review/research-review/SKILL.md +112 -0
  366. package/skills/slides-polish/SKILL.md +565 -0
  367. package/skills/specification-writing/SKILL.md +211 -0
  368. package/skills/system-profile/SKILL.md +103 -0
  369. package/skills/training-check/SKILL.md +132 -0
  370. package/skills/vast-gpu/SKILL.md +394 -0
  371. package/skills/web-debug-search/SKILL.md +334 -0
  372. package/skills/wiki-enrich/SKILL.md +257 -0
  373. package/skills/writing-systems-papers/SKILL.md +184 -0
  374. package/templates/CLAUDE_MD_TEMPLATE.md +29 -0
  375. package/templates/EXPERIMENT_LOG_TEMPLATE.md +47 -0
  376. package/templates/EXPERIMENT_PLAN_TEMPLATE.md +51 -0
  377. package/templates/EXPERIMENT_PLAN_TEMPLATE_CN.md +53 -0
  378. package/templates/FINDINGS_TEMPLATE.md +52 -0
  379. package/templates/IDEA_CANDIDATES_TEMPLATE.md +47 -0
  380. package/templates/IDEA_CANDIDATES_TEMPLATE_CN.md +47 -0
  381. package/templates/INVENTION_BRIEF_TEMPLATE.md +87 -0
  382. package/templates/MANIFEST_TEMPLATE.md +7 -0
  383. package/templates/NARRATIVE_REPORT_TEMPLATE.md +49 -0
  384. package/templates/PAPER_PLAN_TEMPLATE.md +47 -0
  385. package/templates/PATENT_CLAIMS_TEMPLATE.md +78 -0
  386. package/templates/PATENT_SPECIFICATION_TEMPLATE.md +68 -0
  387. package/templates/README.md +57 -0
  388. package/templates/RESEARCH_BRIEF_TEMPLATE.md +35 -0
  389. package/templates/RESEARCH_BRIEF_TEMPLATE_CN.md +41 -0
  390. package/templates/RESEARCH_CONTRACT_TEMPLATE.md +60 -0
  391. package/templates/claude-hooks/corpus_write_guard.json +16 -0
  392. package/templates/claude-hooks/corpus_write_guard.py +85 -0
  393. package/templates/claude-hooks/meta_logging.json +74 -0
  394. package/templates/gitignore-trace.txt +3 -0
  395. package/tools/__pycache__/check_skills_inventory.cpython-314.pyc +0 -0
  396. package/tools/arxiv_fetch.py +311 -0
  397. package/tools/capture_filter.py +126 -0
  398. package/tools/check_skills_inventory.py +273 -0
  399. package/tools/convert_skills_to_llm_chat.py +282 -0
  400. package/tools/copilot_native_evidence.py +818 -0
  401. package/tools/deepxiv_fetch.py +213 -0
  402. package/tools/evidence_check.py +212 -0
  403. package/tools/exa_search.py +425 -0
  404. package/tools/experiment_queue/README.md +118 -0
  405. package/tools/experiment_queue/build_manifest.py +44 -0
  406. package/tools/experiment_queue/queue_manager.py +44 -0
  407. package/tools/extract_paper_style.py +560 -0
  408. package/tools/figure_renderer.py +69 -0
  409. package/tools/forensics_gate.py +669 -0
  410. package/tools/generate_codex_claude_review_overrides.py +299 -0
  411. package/tools/idea_discovery_gate.py +256 -0
  412. package/tools/install_aris.ps1 +1372 -0
  413. package/tools/install_aris.sh +1370 -0
  414. package/tools/install_aris_codex.sh +1023 -0
  415. package/tools/install_aris_copilot.sh +1052 -0
  416. package/tools/iteration_log.py +143 -0
  417. package/tools/lint_skills_helpers.sh +84 -0
  418. package/tools/meta_opt/check_ready.sh +80 -0
  419. package/tools/meta_opt/log_event.sh +91 -0
  420. package/tools/meta_opt/trigger_eval.py +280 -0
  421. package/tools/meta_opt/trigger_evals.sample.json +28 -0
  422. package/tools/openalex_fetch.py +326 -0
  423. package/tools/overleaf_audit.sh +104 -0
  424. package/tools/overleaf_setup.sh +150 -0
  425. package/tools/paper_illustration_image2.py +62 -0
  426. package/tools/provenance.py +294 -0
  427. package/tools/research_wiki.py +1720 -0
  428. package/tools/review_gate.py +502 -0
  429. package/tools/run_state.py +399 -0
  430. package/tools/save_trace.sh +477 -0
  431. package/tools/semantic_scholar_fetch.py +438 -0
  432. package/tools/skill-groups.tsv +116 -0
  433. package/tools/skill_picker.py +238 -0
  434. package/tools/smart_update.ps1 +521 -0
  435. package/tools/smart_update.sh +591 -0
  436. package/tools/smart_update_codex.sh +419 -0
  437. package/tools/smart_update_copilot.sh +605 -0
  438. package/tools/threat_scan.py +222 -0
  439. package/tools/verify_paper_audits.sh +487 -0
  440. package/tools/verify_papers.py +613 -0
  441. package/tools/verify_wiki_coverage.sh +176 -0
  442. package/tools/watchdog.py +485 -0
@@ -0,0 +1,1006 @@
1
+ #!/usr/bin/env python3
2
+ """render_html.py — convert ARIS Markdown / JSON artifacts to single-file HTML.
3
+
4
+ Pure-stdlib Python. No external pip deps.
5
+
6
+ Usage:
7
+ render_html.py <input.md> [--template academic|dashboard]
8
+ [--out <output.html>]
9
+ [--title "..."] [--subtitle "..."]
10
+ [--eyebrow "..."]
11
+ [--state <state.json>]
12
+ [--json <sidecar.json>]
13
+ [--offline]
14
+ [--no-toc]
15
+
16
+ See skills/render-html/SKILL.md for the full contract.
17
+
18
+ Design invariants (see codex review consultation in commit message):
19
+ - Markdown / JSON is canonical source, HTML is generated view.
20
+ - HTML embeds source path + SHA256 + generated timestamp (drift detection).
21
+ - Single-file output. MathJax + highlight.js loaded from CDN unless --offline.
22
+ - Pure stdlib: re, html, hashlib, json, datetime, pathlib, argparse, sys.
23
+ - Conservative Markdown subset matching what ARIS artifacts actually emit.
24
+ """
25
+ from __future__ import annotations
26
+
27
+ import argparse
28
+ import hashlib
29
+ import html as html_lib
30
+ import json
31
+ import re
32
+ import sys
33
+ from datetime import datetime, timezone
34
+ from pathlib import Path
35
+
36
+ TEMPLATES_DIR = Path(__file__).resolve().parent / "templates"
37
+
38
+ # ---------------------------------------------------------------------------
39
+ # Inline parsing
40
+ # ---------------------------------------------------------------------------
41
+
42
+ # Placeholders used to protect inline content during multi-pass rewriting.
43
+ # Two non-printable PUA chars + index to avoid collisions with real content.
44
+ _PH_OPEN = ""
45
+ _PH_CLOSE = ""
46
+
47
+
48
+ def _ph(idx: int) -> str:
49
+ return f"{_PH_OPEN}{idx}{_PH_CLOSE}"
50
+
51
+
52
+ _RE_CODE_INLINE = re.compile(r"`([^`\n]+)`")
53
+ _RE_MATH_DISPLAY = re.compile(r"\$\$([^\n][\s\S]*?)\$\$")
54
+ _RE_MATH_INLINE = re.compile(r"(?<!\\)\$([^\$\n]+?)\$")
55
+ _RE_IMG = re.compile(r"!\[([^\]]*)\]\(([^)\s]+)(?:\s+\"([^\"]*)\")?\)")
56
+ _RE_LINK = re.compile(r"\[([^\]]+)\]\(([^)\s]+)(?:\s+\"([^\"]*)\")?\)")
57
+ _RE_BOLD = re.compile(r"\*\*([^\*\n]+)\*\*")
58
+ _RE_ITALIC = re.compile(r"(?<!\*)\*([^\*\n]+)\*(?!\*)")
59
+ _RE_ITALIC_UNDERSCORE = re.compile(r"(?<!\w)_([^_\n]+)_(?!\w)")
60
+ _RE_STRIKE = re.compile(r"~~([^~\n]+)~~")
61
+ # Wikilink paper reference: [[key]] or [[key|display]]. Backslash-escaped form
62
+ # `\[[...]]` is left alone so authors can opt out.
63
+ _RE_PAPER_REF = re.compile(
64
+ r"(?<!\\)\[\[([A-Za-z0-9][A-Za-z0-9_.:-]*)(?:\|([^\]\n]+))?\]\]"
65
+ )
66
+
67
+ # Inline HTML tags that should pass through inline (commonly used in ARIS docs).
68
+ _INLINE_HTML_TAGS = ("br", "img", "a", "span", "sub", "sup", "code", "kbd", "b", "i", "u", "strong", "em")
69
+
70
+ # URL schemes considered safe for href/src. javascript:, data:, vbscript: blocked.
71
+ _SAFE_URL_SCHEMES = ("http:", "https:", "mailto:", "ftp:", "tel:", "#", "/", "./", "../")
72
+
73
+
74
+ def _safe_url(url: str) -> str:
75
+ """Return url if scheme is safe, else '#'. Defensive against javascript: links."""
76
+ s = url.strip().lower()
77
+ # Relative paths and fragments are fine
78
+ if s.startswith(("#", "/", "./", "../")) or s == "":
79
+ return url
80
+ if s.startswith(_SAFE_URL_SCHEMES):
81
+ return url
82
+ # Bare path like "foo/bar.md" or "page.html" — no scheme prefix, safe.
83
+ if ":" not in s.split("/", 1)[0]:
84
+ return url
85
+ # Block javascript:, data:, vbscript:, file:, etc.
86
+ return "#blocked-unsafe-url"
87
+
88
+
89
+ # Tags stripped wholesale from HTML passthrough (block and inline).
90
+ # Even if the workflow LLM hallucinates these, they never reach output.
91
+ _RE_STRIP_TAG = re.compile(
92
+ r"<\s*(script|style|iframe|object|embed|form|input|button|link|meta|base)\b[^>]*>.*?</\s*\1\s*>",
93
+ re.IGNORECASE | re.DOTALL,
94
+ )
95
+ _RE_STRIP_TAG_SELF = re.compile(
96
+ r"<\s*(script|style|iframe|object|embed|form|input|button|link|meta|base)\b[^>]*/?\s*>",
97
+ re.IGNORECASE,
98
+ )
99
+ # Event-handler attributes like onclick=, onload=, etc. — strip these.
100
+ _RE_STRIP_EVENT_ATTR = re.compile(
101
+ r"\s+on[a-z]+\s*=\s*(\"[^\"]*\"|'[^']*'|[^\s>]+)",
102
+ re.IGNORECASE,
103
+ )
104
+ # javascript:/vbscript:/data: in href/src — replace with #blocked.
105
+ _RE_STRIP_DANGEROUS_URL_ATTR = re.compile(
106
+ r"""(\b(?:href|src|action|formaction|poster)\s*=\s*["']?)\s*(?:javascript|vbscript|data)\s*:""",
107
+ re.IGNORECASE,
108
+ )
109
+
110
+
111
+ def sanitize_html(s: str) -> str:
112
+ """Strip dangerous tags / event handlers / javascript: URLs from raw HTML.
113
+
114
+ Applied to: (a) inline-HTML spans we pass through, (b) block-HTML
115
+ passthrough content. Markdown text content is HTML-escaped separately
116
+ and never reaches this function. ARIS workflow artifacts should not
117
+ contain these tags; this is defense-in-depth in case an LLM hallucinates
118
+ one.
119
+ """
120
+ s = _RE_STRIP_TAG.sub("", s)
121
+ s = _RE_STRIP_TAG_SELF.sub("", s)
122
+ s = _RE_STRIP_EVENT_ATTR.sub("", s)
123
+ s = _RE_STRIP_DANGEROUS_URL_ATTR.sub(r"\1#blocked-unsafe-url:", s)
124
+ return s
125
+
126
+
127
+ def render_inline(text: str) -> str:
128
+ """Convert Markdown inline syntax to HTML. Escape everything else.
129
+
130
+ Order matters:
131
+ 1. Stash inline code, math (display+inline), and raw HTML tag-shaped
132
+ spans into placeholders (so we don't expand markdown inside them).
133
+ 2. HTML-escape the remaining text.
134
+ 3. Apply images, links, bold, italic, strike.
135
+ 4. Restore placeholders.
136
+ """
137
+ stash: list[str] = []
138
+
139
+ def store(replacement: str) -> str:
140
+ idx = len(stash)
141
+ stash.append(replacement)
142
+ return _ph(idx)
143
+
144
+ # 1a. Inline code -- escape inner content fully.
145
+ def _code_sub(m: re.Match[str]) -> str:
146
+ return store(f"<code>{html_lib.escape(m.group(1))}</code>")
147
+
148
+ text = _RE_CODE_INLINE.sub(_code_sub, text)
149
+
150
+ # HTML-escape <, >, & inside math bodies: a bare "<t" (e.g. y_{<t}) is
151
+ # otherwise parsed as an HTML start tag and eats the rest of the formula
152
+ # before MathJax runs. MathJax v3 reads the decoded DOM text, so the escape
153
+ # is transparent to the TeX it sees (and & for cases/align stays intact).
154
+ # 1b. Display math (passthrough; MathJax will render).
155
+ def _md_sub(m: re.Match[str]) -> str:
156
+ body = html_lib.escape(m.group(1), quote=False)
157
+ return store(f"$${body}$$")
158
+
159
+ text = _RE_MATH_DISPLAY.sub(_md_sub, text)
160
+
161
+ # 1c. Inline math (passthrough).
162
+ def _mi_sub(m: re.Match[str]) -> str:
163
+ body = html_lib.escape(m.group(1), quote=False)
164
+ return store(f"${body}$")
165
+
166
+ text = _RE_MATH_INLINE.sub(_mi_sub, text)
167
+
168
+ # 1d. Wikilink paper refs [[key]] or [[key|display]] — stash as a clickable
169
+ # span with data-ref="key". The template JS wires up the popover from
170
+ # window.PAPER_REGISTRY (sidecar JSON loaded via --papers). Default display
171
+ # is the key uppercased; explicit display via |label form.
172
+ def _ref_sub(m: re.Match[str]) -> str:
173
+ key = m.group(1)
174
+ display = m.group(2) if m.group(2) is not None else key.upper()
175
+ return store(
176
+ f'<span data-ref="{html_lib.escape(key, quote=True)}">{html_lib.escape(display)}</span>'
177
+ )
178
+
179
+ text = _RE_PAPER_REF.sub(_ref_sub, text)
180
+
181
+ # 1e. Inline HTML spans (very limited allowlist + sanitize before stash).
182
+ _re_tag = re.compile(
183
+ r"<(/?)(" + "|".join(_INLINE_HTML_TAGS) + r")(\s[^<>]*)?>",
184
+ re.IGNORECASE,
185
+ )
186
+
187
+ def _tag_sub(m: re.Match[str]) -> str:
188
+ return store(sanitize_html(m.group(0)))
189
+
190
+ text = _re_tag.sub(_tag_sub, text)
191
+
192
+ # 2. HTML-escape remainder.
193
+ text = html_lib.escape(text, quote=False)
194
+
195
+ # 3. Apply markdown emphasis & links.
196
+ def _img_sub(m: re.Match[str]) -> str:
197
+ alt = html_lib.escape(m.group(1), quote=True)
198
+ src = html_lib.escape(_safe_url(m.group(2)), quote=True)
199
+ title = m.group(3)
200
+ title_attr = f' title="{html_lib.escape(title, quote=True)}"' if title else ""
201
+ return f'<img src="{src}" alt="{alt}"{title_attr} />'
202
+
203
+ text = _RE_IMG.sub(_img_sub, text)
204
+
205
+ def _link_sub(m: re.Match[str]) -> str:
206
+ label = m.group(1) # inner label can still contain code placeholders; ok.
207
+ href = html_lib.escape(_safe_url(m.group(2)), quote=True)
208
+ title = m.group(3)
209
+ title_attr = f' title="{html_lib.escape(title, quote=True)}"' if title else ""
210
+ return f'<a href="{href}"{title_attr}>{label}</a>'
211
+
212
+ text = _RE_LINK.sub(_link_sub, text)
213
+ text = _RE_BOLD.sub(r"<strong>\1</strong>", text)
214
+ text = _RE_ITALIC.sub(r"<em>\1</em>", text)
215
+ text = _RE_ITALIC_UNDERSCORE.sub(r"<em>\1</em>", text)
216
+ text = _RE_STRIKE.sub(r"<del>\1</del>", text)
217
+
218
+ # 4. Restore placeholders.
219
+ def _restore(m: re.Match[str]) -> str:
220
+ idx = int(m.group(1))
221
+ return stash[idx]
222
+
223
+ text = re.sub(rf"{_PH_OPEN}(\d+){_PH_CLOSE}", _restore, text)
224
+ return text
225
+
226
+
227
+ # ---------------------------------------------------------------------------
228
+ # Block parsing
229
+ # ---------------------------------------------------------------------------
230
+
231
+ _RE_HEADING = re.compile(r"^(#{1,6})\s+(.+?)\s*#*\s*$")
232
+ _RE_HR = re.compile(r"^\s*(?:-{3,}|\*{3,}|_{3,})\s*$")
233
+ _RE_CODE_FENCE = re.compile(
234
+ r"^```\s*(?:(?P<lang>[A-Za-z0-9_+.#-]+)\s*)?(?:\{(?P<flags>[^}\n]+)\}\s*)?$"
235
+ )
236
+ _RE_TABLE_DIVIDER = re.compile(r"^\s*\|?\s*:?-+:?\s*(\|\s*:?-+:?\s*)+\|?\s*$")
237
+ _RE_ORDERED = re.compile(r"^(\s*)(\d+)[.)]\s+(.*)$")
238
+ _RE_UNORDERED = re.compile(r"^(\s*)[-*+]\s+(.*)$")
239
+ _RE_BLOCKQUOTE = re.compile(r"^>\s?(.*)$")
240
+ _RE_HTML_BLOCK_OPEN = re.compile(
241
+ r"^\s*<(details|div|figure|table|p|ul|ol|nav|section|aside|header|footer|main|article|blockquote)(\s|>|/>)",
242
+ re.IGNORECASE,
243
+ )
244
+
245
+ _CALLOUT_PREFIX_MAP = [
246
+ # (regex on first content, css class, default title)
247
+ (re.compile(r"^[⚠️⚠]️?\s*"), "callout-warn", "Warning"),
248
+ (re.compile(r"^💡\s*"), "callout-info", "Tip"),
249
+ (re.compile(r"^✅\s*"), "callout-good", "OK"),
250
+ (re.compile(r"^✓\s*"), "callout-good", "OK"),
251
+ (re.compile(r"^❌\s*"), "callout-bad", "Blocked"),
252
+ (re.compile(r"^🔒\s*"), "callout-good", "Guarantee"),
253
+ (re.compile(r"^📝\s*"), "callout-info", "Note"),
254
+ (re.compile(r"^🚨\s*"), "callout-bad", "Critical"),
255
+ (re.compile(r"^🛠\s*"), "callout-info", "Note"),
256
+ (re.compile(r"^🆕\s*"), "callout-info", "New"),
257
+ (re.compile(r"^⚙️⚡?\s*"), "callout-info", "Config"),
258
+ (re.compile(r"^🔁\s*"), "callout-info", "Loop"),
259
+ (re.compile(r"^🌱\s*"), "callout-info", "Note"),
260
+ (re.compile(r"^📚\s*"), "callout-info", "Reference"),
261
+ (re.compile(r"^🧬\s*"), "callout-info", "Meta"),
262
+ ]
263
+
264
+
265
+ def _slugify(text: str) -> str:
266
+ text = re.sub(r"[`*_~$]", "", text)
267
+ text = re.sub(r"\s+", "-", text.strip().lower())
268
+ text = re.sub(r"[^a-z0-9一-鿿\-]", "", text)
269
+ return text.strip("-") or "section"
270
+
271
+
272
+ def parse_blocks(lines: list[str]) -> list[dict]:
273
+ blocks: list[dict] = []
274
+ i = 0
275
+ n = len(lines)
276
+ while i < n:
277
+ line = lines[i]
278
+ stripped = line.rstrip()
279
+
280
+ # Blank line — skip.
281
+ if not stripped.strip():
282
+ i += 1
283
+ continue
284
+
285
+ # Code fence.
286
+ m = _RE_CODE_FENCE.match(stripped)
287
+ if m:
288
+ lang = (m.group("lang") or "").strip()
289
+ flags_raw = (m.group("flags") or "").strip()
290
+ flags: set[str] = set()
291
+ if flags_raw:
292
+ for tok in flags_raw.split(","):
293
+ tok = tok.strip()
294
+ if tok:
295
+ flags.add(tok)
296
+ body: list[str] = []
297
+ i += 1
298
+ while i < n and not _RE_CODE_FENCE.match(lines[i].rstrip()):
299
+ body.append(lines[i])
300
+ i += 1
301
+ if i < n: # consume closing fence
302
+ i += 1
303
+ blocks.append({
304
+ "type": "code",
305
+ "lang": lang,
306
+ "content": "\n".join(body),
307
+ "flags": flags,
308
+ })
309
+ continue
310
+
311
+ # ATX heading.
312
+ m = _RE_HEADING.match(stripped)
313
+ if m:
314
+ level = len(m.group(1))
315
+ text = m.group(2).strip()
316
+ blocks.append({"type": "heading", "level": level, "text": text})
317
+ i += 1
318
+ continue
319
+
320
+ # Horizontal rule.
321
+ if _RE_HR.match(stripped):
322
+ blocks.append({"type": "hr"})
323
+ i += 1
324
+ continue
325
+
326
+ # Table: current line has at least one `|`, AND next line is a divider.
327
+ if "|" in stripped and i + 1 < n and _RE_TABLE_DIVIDER.match(lines[i + 1].rstrip()):
328
+ header = stripped
329
+ divider = lines[i + 1].rstrip()
330
+ rows: list[str] = []
331
+ j = i + 2
332
+ while j < n and "|" in lines[j] and lines[j].strip():
333
+ rows.append(lines[j].rstrip())
334
+ j += 1
335
+ blocks.append({"type": "table", "header": header, "divider": divider, "rows": rows})
336
+ i = j
337
+ continue
338
+
339
+ # HTML block — passthrough until the closing tag is reached. Per
340
+ # CommonMark §4.6 (loosely): treat the block as ending on the first
341
+ # line that *starts* with the matching </tag>. We track depth only
342
+ # for opening/closing tags that appear at line start (after optional
343
+ # whitespace), so inline mentions like `<details>` inside backticks
344
+ # in a paragraph don't perturb the counter.
345
+ if _RE_HTML_BLOCK_OPEN.match(line):
346
+ tag_m = _RE_HTML_BLOCK_OPEN.match(line)
347
+ assert tag_m is not None
348
+ tag = tag_m.group(1).lower()
349
+ line_open_re = re.compile(rf"^\s*<{re.escape(tag)}(?=[\s/>])", re.IGNORECASE)
350
+ line_close_re = re.compile(rf"^\s*</{re.escape(tag)}(?=[\s>])", re.IGNORECASE)
351
+ body = [line]
352
+ depth = 1
353
+ # Self-closing tag on the same line: <tag .../>
354
+ if re.search(rf"<{re.escape(tag)}\b[^>]*/\s*>", line, re.IGNORECASE):
355
+ depth = 0
356
+ # Same-line full close: <tag>...</tag> or </tag> on same line as open
357
+ elif re.search(rf"</{re.escape(tag)}(?=[\s>])", line, re.IGNORECASE):
358
+ depth = 0
359
+ i += 1
360
+ while i < n and depth > 0:
361
+ cur = lines[i]
362
+ body.append(cur)
363
+ # Count nested opens/closes at line start only.
364
+ if line_open_re.match(cur):
365
+ depth += 1
366
+ if line_close_re.match(cur):
367
+ depth -= 1
368
+ i += 1
369
+ content = "\n".join(body)
370
+ # Sanitize: strip dangerous tags / event handlers / unsafe URL schemes.
371
+ content = sanitize_html(content)
372
+ blocks.append({"type": "html", "content": content})
373
+ continue
374
+
375
+ # Blockquote.
376
+ if _RE_BLOCKQUOTE.match(stripped):
377
+ quote_lines: list[str] = []
378
+ while i < n:
379
+ m_bq = _RE_BLOCKQUOTE.match(lines[i].rstrip())
380
+ if not m_bq:
381
+ break
382
+ quote_lines.append(m_bq.group(1))
383
+ i += 1
384
+ blocks.append({"type": "blockquote", "lines": quote_lines})
385
+ continue
386
+
387
+ # Ordered or unordered list. Indent-aware: a marker at the same
388
+ # indent as the first item starts a sibling; a marker at deeper
389
+ # indent (or any deeper-indented continuation line) becomes part
390
+ # of the current item's content and is re-parsed recursively in
391
+ # render_list (so nested lists / paragraphs / code blocks work).
392
+ if _RE_ORDERED.match(stripped) or _RE_UNORDERED.match(stripped):
393
+ first_m = _RE_ORDERED.match(lines[i]) or _RE_UNORDERED.match(lines[i])
394
+ base_indent = len(first_m.group(1))
395
+ ordered = bool(_RE_ORDERED.match(lines[i]))
396
+ items: list[str] = []
397
+ current: list[str] = []
398
+ while i < n:
399
+ cur_line = lines[i]
400
+ # Blank line — peek ahead to decide if list continues.
401
+ if cur_line.strip() == "":
402
+ if i + 1 < n:
403
+ nxt = lines[i + 1]
404
+ m_o_next = _RE_ORDERED.match(nxt)
405
+ m_u_next = _RE_UNORDERED.match(nxt)
406
+ if (m_o_next or m_u_next) and len(
407
+ (m_o_next or m_u_next).group(1)
408
+ ) >= base_indent:
409
+ # Continue list; consume blank.
410
+ current.append("")
411
+ i += 1
412
+ continue
413
+ if nxt.startswith(" " * (base_indent + 2)) or nxt.startswith("\t"):
414
+ current.append("")
415
+ i += 1
416
+ continue
417
+ break
418
+ m_o = _RE_ORDERED.match(cur_line)
419
+ m_u = _RE_UNORDERED.match(cur_line)
420
+ if m_o or m_u:
421
+ marker_indent = len((m_o or m_u).group(1))
422
+ if marker_indent == base_indent:
423
+ # Sibling item at same level.
424
+ if current:
425
+ items.append("\n".join(current).rstrip())
426
+ current = []
427
+ current.append((m_o or m_u).group(3 if m_o else 2))
428
+ i += 1
429
+ continue
430
+ if marker_indent > base_indent:
431
+ # Nested item — append the line with reduced indent
432
+ # (strip base_indent + 2 spaces so the nested parse
433
+ # sees the marker at column 0 of its own context).
434
+ strip = base_indent + 2
435
+ current.append(
436
+ cur_line[strip:] if cur_line[:strip].strip() == "" else cur_line.lstrip()
437
+ )
438
+ i += 1
439
+ continue
440
+ # Marker at shallower indent — list ends here.
441
+ break
442
+ # Non-marker line: continuation of current item if indented
443
+ # past base_indent + 2 (or any indent for the "lazy continuation"
444
+ # convention).
445
+ if cur_line.startswith(" " * (base_indent + 2)) or cur_line.startswith("\t"):
446
+ strip = base_indent + 2
447
+ current.append(
448
+ cur_line[strip:] if cur_line[:strip].strip() == "" else cur_line.lstrip()
449
+ )
450
+ i += 1
451
+ continue
452
+ # Anything else at base_indent or shallower → list ends.
453
+ break
454
+ if current:
455
+ items.append("\n".join(current).rstrip())
456
+ blocks.append({"type": "list", "ordered": ordered, "items": items})
457
+ continue
458
+
459
+ # Paragraph: gather lines until blank or block boundary.
460
+ para: list[str] = [stripped]
461
+ i += 1
462
+ while i < n:
463
+ nxt = lines[i].rstrip()
464
+ if not nxt.strip():
465
+ break
466
+ if (_RE_HEADING.match(nxt) or _RE_CODE_FENCE.match(nxt) or _RE_HR.match(nxt) or
467
+ _RE_BLOCKQUOTE.match(nxt) or _RE_ORDERED.match(nxt) or
468
+ _RE_UNORDERED.match(nxt) or _RE_HTML_BLOCK_OPEN.match(lines[i]) or
469
+ ("|" in nxt and i + 1 < n and _RE_TABLE_DIVIDER.match(lines[i + 1].rstrip()))):
470
+ break
471
+ para.append(nxt)
472
+ i += 1
473
+ blocks.append({"type": "paragraph", "text": " ".join(para)})
474
+ return blocks
475
+
476
+
477
+ # ---------------------------------------------------------------------------
478
+ # Block rendering
479
+ # ---------------------------------------------------------------------------
480
+
481
+
482
+ def render_table(header: str, divider: str, rows: list[str]) -> str:
483
+ def split_row(s: str) -> list[str]:
484
+ s = s.strip()
485
+ if s.startswith("|"):
486
+ s = s[1:]
487
+ if s.endswith("|"):
488
+ s = s[:-1]
489
+ return [c.strip() for c in s.split("|")]
490
+
491
+ header_cells = split_row(header)
492
+ align = []
493
+ for cell in split_row(divider):
494
+ cell = cell.strip()
495
+ if cell.startswith(":") and cell.endswith(":"):
496
+ align.append("center")
497
+ elif cell.endswith(":"):
498
+ align.append("right")
499
+ else:
500
+ align.append("left")
501
+
502
+ def cell_attr(idx: int) -> str:
503
+ if idx < len(align) and align[idx] != "left":
504
+ return f' style="text-align:{align[idx]}"'
505
+ return ""
506
+
507
+ out: list[str] = ["<table>", "<thead><tr>"]
508
+ for idx, cell in enumerate(header_cells):
509
+ out.append(f"<th{cell_attr(idx)}>{render_inline(cell)}</th>")
510
+ out.append("</tr></thead>")
511
+ out.append("<tbody>")
512
+ for row in rows:
513
+ out.append("<tr>")
514
+ for idx, cell in enumerate(split_row(row)):
515
+ out.append(f"<td{cell_attr(idx)}>{render_inline(cell)}</td>")
516
+ out.append("</tr>")
517
+ out.append("</tbody></table>")
518
+ return "".join(out)
519
+
520
+
521
+ def render_blockquote(quote_lines: list[str]) -> str:
522
+ """Detect callout style (emoji prefix) and render with callout class."""
523
+ if not quote_lines:
524
+ return "<blockquote></blockquote>"
525
+ first = quote_lines[0]
526
+ css_class = None
527
+ title = None
528
+ body_lines = list(quote_lines)
529
+ # Try matching callout prefix on the first non-empty line.
530
+ first_nonempty_idx = 0
531
+ while first_nonempty_idx < len(body_lines) and not body_lines[first_nonempty_idx].strip():
532
+ first_nonempty_idx += 1
533
+ if first_nonempty_idx < len(body_lines):
534
+ line0 = body_lines[first_nonempty_idx].lstrip()
535
+ for pattern, klass, default_title in _CALLOUT_PREFIX_MAP:
536
+ m = pattern.match(line0)
537
+ if m:
538
+ css_class = klass
539
+ title = default_title
540
+ # Strip emoji + try to extract a **Bold:** title prefix.
541
+ rest = pattern.sub("", line0, count=1)
542
+ m_title = re.match(r"\*\*([^*\n]+?)\*\*[::\s\-—]+\s*", rest)
543
+ if m_title:
544
+ title = m_title.group(1)
545
+ rest = rest[m_title.end():]
546
+ body_lines[first_nonempty_idx] = rest
547
+ break
548
+
549
+ inner_md = "\n".join(body_lines).strip("\n")
550
+ inner_blocks = parse_blocks(inner_md.split("\n")) if inner_md else []
551
+ inner_html, _ = _render_blocks(inner_blocks, collect_toc=False)
552
+ if css_class:
553
+ title_html = f'<div class="callout-title">{html_lib.escape(title or "")}</div>' if title else ""
554
+ return f'<div class="callout {css_class}">{title_html}{inner_html}</div>'
555
+ return f"<blockquote>{inner_html}</blockquote>"
556
+
557
+
558
+ def render_list(ordered: bool, items: list[str]) -> str:
559
+ tag = "ol" if ordered else "ul"
560
+ out = [f"<{tag}>"]
561
+ for item in items:
562
+ # Nested lists are possible — parse the item body as blocks.
563
+ item_blocks = parse_blocks(item.split("\n"))
564
+ # Common case: single paragraph — emit inline directly.
565
+ if len(item_blocks) == 1 and item_blocks[0]["type"] == "paragraph":
566
+ out.append(f"<li>{render_inline(item_blocks[0]['text'])}</li>")
567
+ else:
568
+ inner, _ = _render_blocks(item_blocks, collect_toc=False)
569
+ out.append(f"<li>{inner}</li>")
570
+ out.append(f"</{tag}>")
571
+ return "".join(out)
572
+
573
+
574
+ def render_code(lang: str, content: str, flags: set | None = None) -> str:
575
+ escaped = html_lib.escape(content)
576
+ # Per-block override for the auto-collapse JS in the template:
577
+ # ```python {collapsed} → force fold
578
+ # ```python {open} → force expanded
579
+ # Anything else leaves the decision to the auto-threshold (default 30 lines).
580
+ attr = ""
581
+ if flags:
582
+ if "collapsed" in flags:
583
+ attr = ' data-collapse="collapsed"'
584
+ elif "open" in flags:
585
+ attr = ' data-collapse="open"'
586
+ if lang:
587
+ return f'<pre{attr}><code class="language-{html_lib.escape(lang, quote=True)}">{escaped}</code></pre>'
588
+ # Heuristic: if content looks like an ASCII art diagram (mostly box-drawing
589
+ # chars and pipes), tag the surrounding <pre> with class="diagram".
590
+ diagram_chars = set("│─┌┐└┘├┤┬┴┼▲▼◀▶━┃┏┓┗┛╭╮╰╯═║╔╗╚╝╠╣╦╩╬║▶▼─")
591
+ sample = content[:200]
592
+ if sample and sum(1 for c in sample if c in diagram_chars) >= 4:
593
+ return f'<pre{attr} class="diagram"><code>{escaped}</code></pre>'
594
+ return f"<pre{attr}><code>{escaped}</code></pre>"
595
+
596
+
597
+ def _render_blocks(
598
+ blocks: list[dict],
599
+ collect_toc: bool,
600
+ used_ids: dict | None = None,
601
+ ) -> tuple[str, list[dict]]:
602
+ if used_ids is None:
603
+ used_ids = {}
604
+ out: list[str] = []
605
+ toc: list[dict] = []
606
+ for b in blocks:
607
+ t = b["type"]
608
+ if t == "heading":
609
+ level = b["level"]
610
+ text = b["text"]
611
+ inline = render_inline(text)
612
+ base_id = _slugify(text)
613
+ uid = base_id
614
+ n = used_ids.get(base_id, 0)
615
+ if n > 0:
616
+ uid = f"{base_id}-{n}"
617
+ used_ids[base_id] = n + 1
618
+ out.append(f'<h{level} id="{uid}">{inline}</h{level}>')
619
+ if collect_toc and 2 <= level <= 3:
620
+ toc.append({"level": level, "id": uid, "text": text})
621
+ elif t == "paragraph":
622
+ out.append(f"<p>{render_inline(b['text'])}</p>")
623
+ elif t == "hr":
624
+ out.append("<hr />")
625
+ elif t == "code":
626
+ out.append(render_code(b.get("lang", ""), b["content"], b.get("flags")))
627
+ elif t == "blockquote":
628
+ out.append(render_blockquote(b["lines"]))
629
+ elif t == "list":
630
+ out.append(render_list(b["ordered"], b["items"]))
631
+ elif t == "table":
632
+ out.append(render_table(b["header"], b["divider"], b["rows"]))
633
+ elif t == "html":
634
+ out.append(_render_html_block(b["content"]))
635
+ else:
636
+ out.append(f"<!-- unknown block: {html_lib.escape(t)} -->")
637
+ return "\n".join(out), toc
638
+
639
+
640
+ def _render_html_block(content: str) -> str:
641
+ """For <details>...</details> blocks, parse the inner content as Markdown.
642
+
643
+ Matches GitHub-flavored convention: when a `<details>` block has a blank
644
+ line separating its `<summary>` from the body, the body is parsed as
645
+ markdown (lists, headings, code, callouts all work). For non-<details>
646
+ HTML blocks, content passes through verbatim.
647
+ """
648
+ lines = content.split("\n")
649
+ if not lines or not lines[0].lstrip().lower().startswith("<details"):
650
+ return content
651
+ # Locate opening boundary: after </summary> if present, else after the
652
+ # <details> line itself.
653
+ open_end = 1 # default: just past the <details> line
654
+ summary_close_re = re.compile(r"</summary\s*>", re.IGNORECASE)
655
+ for j in range(1, len(lines)):
656
+ if summary_close_re.search(lines[j]):
657
+ open_end = j + 1
658
+ break
659
+ # If we hit non-summary content first, no `<summary>` block; stop scanning.
660
+ if lines[j].strip() and not re.match(r"\s*<summary", lines[j], re.IGNORECASE):
661
+ break
662
+ # Locate closing boundary: the last </details> line.
663
+ close_start = len(lines) - 1
664
+ close_re = re.compile(r"^\s*</details\s*>", re.IGNORECASE)
665
+ while close_start > 0 and not close_re.match(lines[close_start]):
666
+ close_start -= 1
667
+ if open_end >= close_start:
668
+ # Degenerate or empty body; emit raw.
669
+ return content
670
+ open_part = "\n".join(lines[:open_end])
671
+ inner_md = "\n".join(lines[open_end:close_start]).strip("\n")
672
+ close_part = "\n".join(lines[close_start:])
673
+ if not inner_md.strip():
674
+ return content
675
+ inner_blocks = parse_blocks(inner_md.split("\n"))
676
+ inner_html, _ = _render_blocks(inner_blocks, collect_toc=False)
677
+ return f"{open_part}\n{inner_html}\n{close_part}"
678
+
679
+
680
+ def render_toc(toc: list[dict]) -> str:
681
+ """Render a nested TOC. Groups consecutive H3s under their preceding H2.
682
+
683
+ Orphan H3 (H3 without a preceding H2 in this run) is promoted to a
684
+ top-level <li>. Output is well-formed `<ol><li>...<ul><li>...</li></ul>...</li></ol>`.
685
+ """
686
+ if not toc:
687
+ return ""
688
+ grouped: list[tuple[dict, list[dict]]] = [] # [(h2_entry, [h3_children])]
689
+ current_parent: dict | None = None
690
+ current_children: list[dict] = []
691
+ for entry in toc:
692
+ if entry["level"] == 2:
693
+ if current_parent is not None:
694
+ grouped.append((current_parent, current_children))
695
+ current_parent = entry
696
+ current_children = []
697
+ elif entry["level"] == 3:
698
+ if current_parent is None:
699
+ # Orphan H3 — promote to top level so the TOC stays well-formed.
700
+ grouped.append((entry, []))
701
+ else:
702
+ current_children.append(entry)
703
+ if current_parent is not None:
704
+ grouped.append((current_parent, current_children))
705
+
706
+ out: list[str] = ["<ol>"]
707
+ for parent, children in grouped:
708
+ pid = html_lib.escape(parent["id"], quote=True)
709
+ ptext = html_lib.escape(parent["text"])
710
+ out.append(f'<li><a href="#{pid}">{ptext}</a>')
711
+ if children:
712
+ out.append("<ul>")
713
+ for c in children:
714
+ cid = html_lib.escape(c["id"], quote=True)
715
+ ctext = html_lib.escape(c["text"])
716
+ out.append(f'<li><a href="#{cid}">{ctext}</a></li>')
717
+ out.append("</ul>")
718
+ out.append("</li>")
719
+ out.append("</ol>")
720
+ return "\n".join(out)
721
+
722
+
723
+ def strip_frontmatter(md: str) -> str:
724
+ """Strip leading YAML frontmatter (--- ... ---) if present at start.
725
+
726
+ Also strips a leading UTF-8 BOM so frontmatter detection still fires
727
+ on files saved by editors that prepend it.
728
+ """
729
+ md = md.lstrip("")
730
+ lines = md.split("\n", 1)
731
+ if not lines or lines[0].strip() != "---":
732
+ return md
733
+ # find closing ---
734
+ rest = lines[1] if len(lines) > 1 else ""
735
+ parts = rest.split("\n---\n", 1)
736
+ if len(parts) == 2:
737
+ return parts[1].lstrip("\n")
738
+ # try also \n--- at the very end of a line
739
+ parts = rest.split("\n---", 1)
740
+ if len(parts) == 2 and (parts[1] == "" or parts[1].startswith("\n")):
741
+ return parts[1].lstrip("\n")
742
+ return md
743
+
744
+
745
+ # ---------------------------------------------------------------------------
746
+ # Template loading + main
747
+ # ---------------------------------------------------------------------------
748
+
749
+ CDN_BLOCK_FULL = """<!-- MathJax 3 -->
750
+ <script>
751
+ window.MathJax = {
752
+ tex: { inlineMath: [['$', '$'], ['\\\\(', '\\\\)']], displayMath: [['$$', '$$'], ['\\\\[', '\\\\]']], processEscapes: true },
753
+ options: { skipHtmlTags: ['script', 'noscript', 'style', 'textarea', 'pre', 'code'] }
754
+ };
755
+ </script>
756
+ <script src="https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-mml-chtml.js" async></script>
757
+
758
+ <!-- highlight.js -->
759
+ <link rel="stylesheet" href="https://cdn.jsdelivr.net/gh/highlightjs/cdn-release@11.9.0/build/styles/atom-one-light.min.css">
760
+ <script src="https://cdn.jsdelivr.net/gh/highlightjs/cdn-release@11.9.0/build/highlight.min.js"></script>
761
+ <script>document.addEventListener('DOMContentLoaded', () => hljs.highlightAll());</script>
762
+ """
763
+
764
+ CDN_BLOCK_OFFLINE = "<!-- offline mode: MathJax + highlight.js skipped; math and code blocks render as plain text -->"
765
+
766
+
767
+ def load_template(name: str) -> str:
768
+ path = TEMPLATES_DIR / f"{name}.html"
769
+ if not path.exists():
770
+ raise SystemExit(
771
+ f"error: template '{name}' not found at {path}. "
772
+ f"Available: {[p.stem for p in TEMPLATES_DIR.glob('*.html')]}"
773
+ )
774
+ return path.read_text(encoding="utf-8")
775
+
776
+
777
+ def sha256_of(text: str) -> str:
778
+ return hashlib.sha256(text.encode("utf-8")).hexdigest()
779
+
780
+
781
+ def render_json_as_pre(json_path: Path) -> str:
782
+ """Render a sidecar JSON file as a collapsed <details> block of pretty-printed JSON."""
783
+ try:
784
+ obj = json.loads(json_path.read_text(encoding="utf-8"))
785
+ except json.JSONDecodeError as e:
786
+ return f'<div class="callout callout-bad"><div class="callout-title">JSON parse error</div><p>{html_lib.escape(str(e))}</p></div>'
787
+ pretty = json.dumps(obj, ensure_ascii=False, indent=2, sort_keys=False)
788
+ return (
789
+ f'<details><summary>Sidecar JSON: <code>{html_lib.escape(str(json_path))}</code></summary>'
790
+ f'<pre><code class="language-json">{html_lib.escape(pretty)}</code></pre>'
791
+ f'</details>'
792
+ )
793
+
794
+
795
+ def substitute(template: str, vars: dict) -> str:
796
+ out = template
797
+ for k, v in vars.items():
798
+ out = out.replace("{{" + k + "}}", v)
799
+ return out
800
+
801
+
802
+ def json_for_script(obj: object) -> str:
803
+ """Serialize JSON for direct embedding inside a classic <script> block.
804
+
805
+ json.dumps() alone is unsafe because a string value containing '</script>'
806
+ will break out of the host <script> tag. Escape <, >, & and line/paragraph
807
+ separators as \\uXXXX so the result is always safe inside a <script>.
808
+ """
809
+ return (
810
+ json.dumps(obj, ensure_ascii=False)
811
+ .replace("&", "\\u0026")
812
+ .replace("<", "\\u003c")
813
+ .replace(">", "\\u003e")
814
+ .replace("
", "\\u2028")
815
+ .replace("
", "\\u2029")
816
+ )
817
+
818
+
819
+ def _repo_relative(input_path: Path) -> str:
820
+ """Return a display-friendly repo-relative path. Avoids leaking absolute
821
+ /Users/<name>/... in the generated HTML meta + footer.
822
+
823
+ Order: cwd-relative if input is under cwd; else git-root-relative if in
824
+ a git repo; else basename only (parent dirs stripped) so we never
825
+ surface the home directory.
826
+ """
827
+ try:
828
+ return str(input_path.relative_to(Path.cwd()))
829
+ except ValueError:
830
+ pass
831
+ try:
832
+ import subprocess
833
+ result = subprocess.run(
834
+ ["git", "rev-parse", "--show-toplevel"],
835
+ cwd=input_path.parent,
836
+ capture_output=True,
837
+ text=True,
838
+ check=False,
839
+ )
840
+ if result.returncode == 0:
841
+ git_root = Path(result.stdout.strip())
842
+ try:
843
+ return str(input_path.relative_to(git_root))
844
+ except ValueError:
845
+ pass
846
+ except Exception:
847
+ pass
848
+ return input_path.name
849
+
850
+
851
+ def main(argv: list[str] | None = None) -> int:
852
+ ap = argparse.ArgumentParser(
853
+ description="Render an ARIS Markdown artifact to single-file HTML.",
854
+ )
855
+ ap.add_argument("input", help="Path to input .md (or .json — wrapped in a <pre>)")
856
+ ap.add_argument("--template", default="academic", choices=["academic", "dashboard"])
857
+ ap.add_argument("--out", help="Output HTML path (default: <input>.html)")
858
+ ap.add_argument("--title", help="Page title (default: first H1, or filename)")
859
+ ap.add_argument("--subtitle", default="", help="Optional italic subtitle line")
860
+ ap.add_argument("--eyebrow", default="", help="Optional uppercase eyebrow above H1")
861
+ ap.add_argument("--author", default="", help="Optional author byline (e.g., 'Name (姓名), Affiliation')")
862
+ ap.add_argument("--lang", default="zh-CN", help='<html lang="…"> attribute (default zh-CN)')
863
+ ap.add_argument("--state", help="Optional sidecar state JSON to append as <details>")
864
+ ap.add_argument("--json", dest="json_sidecar", help="Optional sidecar JSON to append (e.g., KILL_ARGUMENT.json)")
865
+ ap.add_argument("--offline", action="store_true", help="Skip MathJax / highlight.js CDN blocks")
866
+ ap.add_argument("--no-toc", action="store_true", help="Skip TOC sidebar (forces TOC_LABEL/TOC_HTML to empty)")
867
+ ap.add_argument(
868
+ "--papers",
869
+ help="Sidecar JSON file with paper registry for [[key]] popovers. "
870
+ "Schema: {key: {title, authors, date, inst, key, arxiv}, ...}",
871
+ )
872
+ ap.add_argument(
873
+ "--blog-mode",
874
+ action="store_true",
875
+ help="Enable blog/talk mode (adds .aris-blog body class, opt-in active-H2 highlighting)",
876
+ )
877
+ ap.add_argument(
878
+ "--collapse-code-min",
879
+ type=int,
880
+ default=30,
881
+ help="Auto-collapse code blocks with >= N lines (default 30). Override per-block with ```lang {open}/{collapsed}.",
882
+ )
883
+ args = ap.parse_args(argv)
884
+
885
+ input_path = Path(args.input).resolve()
886
+ if not input_path.exists():
887
+ print(f"error: input not found: {input_path}", file=sys.stderr)
888
+ return 2
889
+ display_source_path = _repo_relative(input_path)
890
+
891
+ raw = input_path.read_text(encoding="utf-8")
892
+ source_hash = sha256_of(raw)
893
+
894
+ # If the input is JSON, wrap it as a single code block.
895
+ is_json = input_path.suffix.lower() == ".json"
896
+ if is_json:
897
+ try:
898
+ obj = json.loads(raw)
899
+ pretty = json.dumps(obj, ensure_ascii=False, indent=2, sort_keys=False)
900
+ except json.JSONDecodeError:
901
+ pretty = raw
902
+ md_source = f"# {input_path.name}\n\n```json\n{pretty}\n```\n"
903
+ else:
904
+ md_source = strip_frontmatter(raw)
905
+
906
+ blocks = parse_blocks(md_source.split("\n"))
907
+ body_html, toc = _render_blocks(blocks, collect_toc=not args.no_toc)
908
+
909
+ # Title autodetection.
910
+ title = args.title
911
+ if not title:
912
+ # Look for first H1 block.
913
+ for b in blocks:
914
+ if b.get("type") == "heading" and b.get("level") == 1:
915
+ title = b["text"]
916
+ break
917
+ if not title:
918
+ title = input_path.stem.replace("_", " ").replace("-", " ").title()
919
+
920
+ # Append sidecar JSON if requested.
921
+ extra_html_blocks: list[str] = []
922
+ for label, path_str in (("state", args.state), ("json", args.json_sidecar)):
923
+ if path_str:
924
+ p = Path(path_str).resolve()
925
+ if p.exists():
926
+ extra_html_blocks.append(f'<h2 id="sidecar-{label}">Sidecar — <code>{html_lib.escape(p.name)}</code></h2>')
927
+ extra_html_blocks.append(render_json_as_pre(p))
928
+ else:
929
+ extra_html_blocks.append(
930
+ f'<div class="callout callout-warn"><div class="callout-title">Sidecar missing</div>'
931
+ f'<p><code>{html_lib.escape(path_str)}</code> not found.</p></div>'
932
+ )
933
+
934
+ body_html = body_html + ("\n" + "\n".join(extra_html_blocks) if extra_html_blocks else "")
935
+
936
+ template_str = load_template(args.template)
937
+
938
+ toc_html = render_toc(toc) if not args.no_toc else ""
939
+ toc_label = "Contents" if not args.no_toc else ""
940
+ if args.no_toc:
941
+ # Hide TOC entirely by emitting an empty <nav> (template has <nav>, we'd
942
+ # rather just leave structure intact; CSS won't visually break).
943
+ toc_html = ""
944
+
945
+ eyebrow_block = f'<div class="eyebrow">{html_lib.escape(args.eyebrow)}</div>' if args.eyebrow else ""
946
+ subtitle_block = f'<p class="subtitle">{html_lib.escape(args.subtitle)}</p>' if args.subtitle else ""
947
+ byline_block = (
948
+ f'<p class="byline">By <strong>{html_lib.escape(args.author)}</strong></p>'
949
+ if args.author else ""
950
+ )
951
+
952
+ generated_at = datetime.now(timezone.utc).strftime("%Y-%m-%d %H:%M UTC")
953
+
954
+ # Paper registry sidecar (for [[key]] popovers). Validate as JSON before
955
+ # embedding; on parse error, log a warning and emit "{}" so the template
956
+ # still works (popover JS just no-ops when key is missing).
957
+ papers_json = "{}"
958
+ if args.papers:
959
+ p = Path(args.papers).resolve()
960
+ if p.exists():
961
+ try:
962
+ obj = json.loads(p.read_text(encoding="utf-8"))
963
+ papers_json = json_for_script(obj)
964
+ except json.JSONDecodeError as e:
965
+ print(f"warning: --papers JSON parse error: {e}", file=sys.stderr)
966
+ else:
967
+ print(f"warning: --papers file not found: {p}", file=sys.stderr)
968
+
969
+ vars_ = {
970
+ "LANG": html_lib.escape(args.lang, quote=True),
971
+ "TITLE": html_lib.escape(title),
972
+ "SUBTITLE_BLOCK": subtitle_block,
973
+ "EYEBROW_BLOCK": eyebrow_block,
974
+ "BYLINE_BLOCK": byline_block,
975
+ "SOURCE_PATH": html_lib.escape(display_source_path),
976
+ "SOURCE_SHA256": source_hash,
977
+ "SOURCE_SHA256_SHORT": source_hash[:12],
978
+ "GENERATED_AT": generated_at,
979
+ "HEAD_CDN": CDN_BLOCK_OFFLINE if args.offline else CDN_BLOCK_FULL,
980
+ "TOC_HTML": toc_html,
981
+ "TOC_LABEL": toc_label,
982
+ "BODY_HTML": body_html,
983
+ "EXTRA_META": "",
984
+ "PAPER_REGISTRY_JSON": papers_json,
985
+ "COLLAPSE_CODE_MIN": str(args.collapse_code_min),
986
+ "BODY_CLASS": "aris-blog" if args.blog_mode else "",
987
+ }
988
+
989
+ rendered = substitute(template_str, vars_)
990
+
991
+ out_path = Path(args.out).resolve() if args.out else input_path.with_suffix(".html")
992
+ if out_path.is_dir():
993
+ print(
994
+ f"error: --out points to a directory: {out_path}. "
995
+ f"Specify a file path ending in .html.",
996
+ file=sys.stderr,
997
+ )
998
+ return 2
999
+ out_path.parent.mkdir(parents=True, exist_ok=True)
1000
+ out_path.write_text(rendered, encoding="utf-8")
1001
+ print(f"wrote {out_path} ({len(rendered):,} bytes, {len(toc)} TOC entries, source sha256 {source_hash[:12]}...)")
1002
+ return 0
1003
+
1004
+
1005
+ if __name__ == "__main__":
1006
+ sys.exit(main())