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,799 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ ARIS FigureSpec → SVG Renderer v2
4
+
5
+ Converts a FigureSpec JSON into publication-quality SVG for academic papers.
6
+ Deterministic: same spec = same SVG, every time.
7
+
8
+ Usage:
9
+ python3 figure_renderer.py render spec.json [--output figures/output.svg] [--preview]
10
+ python3 figure_renderer.py validate spec.json
11
+ python3 figure_renderer.py schema
12
+ """
13
+
14
+ import argparse
15
+ import json
16
+ import math
17
+ import os
18
+ import re
19
+ import sys
20
+ from pathlib import Path
21
+ from xml.etree.ElementTree import Element, SubElement, tostring
22
+ from xml.dom.minidom import parseString
23
+
24
+
25
+ # ============================================================
26
+ # Constants & Defaults
27
+ # ============================================================
28
+
29
+ ARROW_SIZE = 8
30
+ SELF_LOOP_RADIUS = 25
31
+
32
+ # Allowed values for sanitization
33
+ ALLOWED_SHAPES = {"rect", "rounded", "circle", "diamond", "ellipse"}
34
+ ALLOWED_STYLES = {"solid", "dashed", "dotted"}
35
+ ALLOWED_ANCHORS = {"start", "middle", "end"}
36
+ HEX_COLOR_RE = re.compile(r"^#[0-9a-fA-F]{6}$")
37
+
38
+ DEFAULT_STYLE = {
39
+ "font_family": "Arial, Helvetica, sans-serif",
40
+ "font_size": 14,
41
+ "bg_color": "#FFFFFF",
42
+ "palette": ["#2563EB", "#10B981", "#7C3AED", "#EA580C", "#C62828",
43
+ "#0D47A1", "#1B5E20", "#4A148C", "#BF360C", "#37474F"],
44
+ }
45
+
46
+ DEFAULT_NODE = {
47
+ "width": 120,
48
+ "height": 50,
49
+ "shape": "rounded",
50
+ "text_color": "#333333",
51
+ "font_size": None,
52
+ }
53
+
54
+ DEFAULT_EDGE = {
55
+ "style": "solid",
56
+ "color": "#555555",
57
+ "thickness": 2,
58
+ "curve": False,
59
+ }
60
+
61
+
62
+ # ============================================================
63
+ # Sanitization
64
+ # ============================================================
65
+
66
+ def sanitize_color(val: str, fallback: str = "#555555") -> str:
67
+ """Validate hex color, reject anything else."""
68
+ if isinstance(val, str) and HEX_COLOR_RE.match(val):
69
+ return val
70
+ return fallback
71
+
72
+
73
+ def sanitize_text(val: str) -> str:
74
+ """Strip XML-illegal characters from text. ElementTree escapes &<>, but some code points are invalid in XML."""
75
+ if not isinstance(val, str):
76
+ return str(val)
77
+ # Remove XML-illegal code points: C0 controls (except \t\n\r), C1, surrogates, noncharacters
78
+ val = re.sub(r"[\x00-\x08\x0b\x0c\x0e-\x1f\x7f-\x9f]", "", val)
79
+ # Remove BMP noncharacters (U+FDD0-U+FDEF, U+FFFE-U+FFFF) and surrogates
80
+ val = re.sub(r"[\ud800-\udfff\ufdd0-\ufdef\ufffe\uffff]", "", val)
81
+ return val
82
+
83
+
84
+ def estimate_text_width(text: str, font_size: int) -> float:
85
+ """Estimate text width in px. Rough but consistent."""
86
+ # Average character width ≈ 0.6 × font_size for sans-serif
87
+ # CJK characters ≈ 1.0 × font_size
88
+ width = 0
89
+ for ch in text:
90
+ if ord(ch) > 0x2E80: # CJK range
91
+ width += font_size * 1.0
92
+ else:
93
+ width += font_size * 0.6
94
+ return width
95
+
96
+
97
+ # ============================================================
98
+ # Color Utilities
99
+ # ============================================================
100
+
101
+ def lighten_color(hex_color: str, factor: float = 0.85) -> str:
102
+ """Lighten a hex color for node fills."""
103
+ hex_color = hex_color.lstrip("#")
104
+ r, g, b = int(hex_color[0:2], 16), int(hex_color[2:4], 16), int(hex_color[4:6], 16)
105
+ r = min(255, int(r + (255 - r) * factor))
106
+ g = min(255, int(g + (255 - g) * factor))
107
+ b = min(255, int(b + (255 - b) * factor))
108
+ return f"#{r:02x}{g:02x}{b:02x}"
109
+
110
+
111
+ # ============================================================
112
+ # Geometry: Shape-Aware Edge Clipping
113
+ # ============================================================
114
+
115
+ def clip_to_shape(cx, cy, target_x, target_y, w, h, shape):
116
+ """Clip a line from (cx,cy) toward (target_x,target_y) to the shape boundary."""
117
+ dx = target_x - cx
118
+ dy = target_y - cy
119
+ if dx == 0 and dy == 0:
120
+ return cx, cy - h / 2 # default: top
121
+
122
+ if shape == "circle":
123
+ # True circle: use max(w,h)/2 to match renderer
124
+ r = max(w, h) / 2
125
+ angle = math.atan2(dy, dx)
126
+ return cx + r * math.cos(angle), cy + r * math.sin(angle)
127
+
128
+ elif shape == "ellipse":
129
+ # Ellipse clipping: x²/a² + y²/b² = 1
130
+ a = w / 2
131
+ b = h / 2
132
+ angle = math.atan2(dy, dx)
133
+ return cx + a * math.cos(angle), cy + b * math.sin(angle)
134
+
135
+ elif shape == "diamond":
136
+ # Diamond: |x/a| + |y/b| = 1
137
+ a = w / 2
138
+ b = h / 2
139
+ angle = math.atan2(dy, dx)
140
+ cos_a = abs(math.cos(angle))
141
+ sin_a = abs(math.sin(angle))
142
+ if cos_a * b + sin_a * a == 0:
143
+ return cx, cy
144
+ scale = (a * b) / (cos_a * b + sin_a * a)
145
+ return cx + scale * math.cos(angle), cy + scale * math.sin(angle)
146
+
147
+ else:
148
+ # Rectangle clipping
149
+ a = w / 2
150
+ b = h / 2
151
+ if abs(dx) * b > abs(dy) * a:
152
+ scale = a / abs(dx)
153
+ else:
154
+ scale = b / abs(dy)
155
+ return cx + dx * scale, cy + dy * scale
156
+
157
+
158
+ # ============================================================
159
+ # Validation
160
+ # ============================================================
161
+
162
+ def validate_spec(spec: dict) -> list:
163
+ """Validate FigureSpec, return list of issues."""
164
+ issues = []
165
+
166
+ # Top-level must be a dict
167
+ if not isinstance(spec, dict):
168
+ return [f"CRITICAL: spec must be a JSON object, got {type(spec).__name__}"]
169
+
170
+ # Structure validation — ensure top-level fields are correct types
171
+ canvas = spec.get("canvas", {})
172
+ if not isinstance(canvas, dict):
173
+ issues.append("CRITICAL: 'canvas' must be a dict")
174
+ canvas = {}
175
+ st_raw = spec.get("style", {})
176
+ if not isinstance(st_raw, dict):
177
+ issues.append("CRITICAL: 'style' must be a dict")
178
+ if not isinstance(spec.get("nodes", []), list):
179
+ issues.append("CRITICAL: 'nodes' must be a list")
180
+ if not isinstance(spec.get("edges", []), list):
181
+ issues.append("CRITICAL: 'edges' must be a list")
182
+ if not isinstance(spec.get("groups", []), list):
183
+ issues.append("CRITICAL: 'groups' must be a list")
184
+ if not isinstance(spec.get("labels", []), list):
185
+ issues.append("CRITICAL: 'labels' must be a list")
186
+
187
+ # Early return if structure is fundamentally broken
188
+ if any(i.startswith("CRITICAL: '") for i in issues):
189
+ return issues
190
+
191
+ # Canvas validation
192
+ for dim in ("width", "height"):
193
+ val = canvas.get(dim)
194
+ if val is not None:
195
+ if isinstance(val, bool) or not isinstance(val, (int, float)):
196
+ issues.append(f"CRITICAL: canvas.{dim} must be a number")
197
+ elif val <= 0:
198
+ issues.append(f"CRITICAL: canvas.{dim} must be positive")
199
+
200
+ # Style validation
201
+ st = spec.get("style", {})
202
+ fs = st.get("font_size")
203
+ if fs is not None and (isinstance(fs, bool) or not isinstance(fs, (int, float)) or fs <= 0):
204
+ issues.append(f"CRITICAL: style.font_size must be a positive number")
205
+ pal = st.get("palette")
206
+ if pal is not None:
207
+ if not isinstance(pal, list) or len(pal) == 0:
208
+ issues.append(f"CRITICAL: style.palette must be a non-empty list of hex colors")
209
+ else:
210
+ for pi, pc in enumerate(pal):
211
+ if not isinstance(pc, str) or not HEX_COLOR_RE.match(pc):
212
+ issues.append(f"CRITICAL: style.palette[{pi}] '{pc}' is not a valid hex color (#RRGGBB)")
213
+
214
+ nodes = spec.get("nodes", [])
215
+ if not nodes:
216
+ issues.append("WARN: no nodes defined (labels/groups-only figure)")
217
+
218
+ node_ids = set()
219
+ for i, node in enumerate(nodes):
220
+ if not isinstance(node, dict):
221
+ issues.append(f"CRITICAL: nodes[{i}] must be a dict, got {type(node).__name__}")
222
+ continue
223
+ nid = node.get("id")
224
+ if not nid:
225
+ issues.append(f"CRITICAL: node[{i}] missing 'id'")
226
+ continue
227
+ if nid in node_ids:
228
+ issues.append(f"CRITICAL: duplicate node id '{nid}'")
229
+ node_ids.add(nid)
230
+ if "label" not in node:
231
+ issues.append(f"WARN: node '{nid}' missing 'label'")
232
+ for coord in ("x", "y"):
233
+ if coord not in node:
234
+ issues.append(f"CRITICAL: node '{nid}' missing '{coord}'")
235
+ elif isinstance(node[coord], bool) or not isinstance(node[coord], (int, float)):
236
+ issues.append(f"CRITICAL: node '{nid}' {coord} must be a number, got {type(node[coord]).__name__}")
237
+ for dim in ("width", "height"):
238
+ val = node.get(dim)
239
+ if val is not None and (isinstance(val, bool) or not isinstance(val, (int, float))):
240
+ issues.append(f"CRITICAL: node '{nid}' {dim} must be a number, got {type(val).__name__}")
241
+ elif val is not None and val <= 0:
242
+ issues.append(f"CRITICAL: node '{nid}' {dim} must be positive ({val})")
243
+ shape = node.get("shape", "rounded")
244
+ if shape not in ALLOWED_SHAPES:
245
+ issues.append(f"WARN: node '{nid}' unknown shape '{shape}', will use 'rounded'")
246
+
247
+ for i, edge in enumerate(spec.get("edges", [])):
248
+ if not isinstance(edge, dict):
249
+ issues.append(f"CRITICAL: edges[{i}] must be a dict")
250
+ continue
251
+ src, dst = edge.get("from"), edge.get("to")
252
+ if not src or not dst:
253
+ issues.append(f"CRITICAL: edge[{i}] missing 'from' or 'to'")
254
+ else:
255
+ if src not in node_ids:
256
+ issues.append(f"CRITICAL: edge[{i}] 'from' references unknown node '{src}'")
257
+ if dst not in node_ids:
258
+ issues.append(f"CRITICAL: edge[{i}] 'to' references unknown node '{dst}'")
259
+ style = edge.get("style", "solid")
260
+ if style not in ALLOWED_STYLES:
261
+ issues.append(f"WARN: edge[{i}] unknown style '{style}', will use 'solid'")
262
+
263
+ for i, group in enumerate(spec.get("groups", [])):
264
+ if not isinstance(group, dict):
265
+ issues.append(f"CRITICAL: groups[{i}] must be a dict")
266
+ continue
267
+ node_ids_list = group.get("node_ids", [])
268
+ if not isinstance(node_ids_list, list):
269
+ issues.append(f"WARN: group[{i}] node_ids must be a list")
270
+ continue
271
+ for nid in node_ids_list:
272
+ if nid not in node_ids:
273
+ issues.append(f"WARN: group[{i}] references unknown node '{nid}'")
274
+
275
+ # Edge numeric validation
276
+ for i, edge in enumerate(spec.get("edges", [])):
277
+ for field in ("thickness",):
278
+ val = edge.get(field)
279
+ if val is not None and (isinstance(val, bool) or not isinstance(val, (int, float))):
280
+ issues.append(f"WARN: edge[{i}] {field} must be a number")
281
+
282
+ # Group numeric validation
283
+ for i, group in enumerate(spec.get("groups", [])):
284
+ val = group.get("padding")
285
+ if val is not None and (isinstance(val, bool) or not isinstance(val, (int, float))):
286
+ issues.append(f"WARN: group[{i}] padding must be a number")
287
+
288
+ # Free label numeric + anchor validation
289
+ for i, label in enumerate(spec.get("labels", [])):
290
+ if not isinstance(label, dict):
291
+ issues.append(f"CRITICAL: labels[{i}] must be a dict")
292
+ continue
293
+ anchor = label.get("anchor")
294
+ if anchor and anchor not in ALLOWED_ANCHORS:
295
+ issues.append(f"WARN: label[{i}] unknown anchor '{anchor}', will use 'middle'")
296
+ for field in ("x", "y", "font_size"):
297
+ val = label.get(field)
298
+ if val is not None and (isinstance(val, bool) or not isinstance(val, (int, float))):
299
+ issues.append(f"WARN: label[{i}] {field} must be a number")
300
+
301
+ # Node font_size validation
302
+ for node in nodes:
303
+ if not isinstance(node, dict):
304
+ continue
305
+ nid = node.get("id", "?")
306
+ nfs = node.get("font_size")
307
+ if nfs is not None and (isinstance(nfs, bool) or not isinstance(nfs, (int, float)) or nfs <= 0):
308
+ issues.append(f"WARN: node '{nid}' font_size must be a positive number")
309
+
310
+ # Overlap detection (shape-aware bounding for circles)
311
+ def _effective_bounds(n):
312
+ w = n.get("width", DEFAULT_NODE["width"])
313
+ h = n.get("height", DEFAULT_NODE["height"])
314
+ if n.get("shape") == "circle":
315
+ d = max(w, h)
316
+ return d, d
317
+ return w, h
318
+
319
+ for i, a in enumerate(nodes):
320
+ if not isinstance(a, dict):
321
+ continue
322
+ for j, b in enumerate(nodes):
323
+ if i >= j or not isinstance(b, dict):
324
+ continue
325
+ ax, ay = a.get("x", 0), a.get("y", 0)
326
+ bx, by = b.get("x", 0), b.get("y", 0)
327
+ aw, ah = _effective_bounds(a)
328
+ bw, bh = _effective_bounds(b)
329
+ if (abs(ax - bx) < (aw + bw) / 2 - 5 and
330
+ abs(ay - by) < (ah + bh) / 2 - 5):
331
+ issues.append(f"WARN: nodes '{a.get('id')}' and '{b.get('id')}' may overlap")
332
+
333
+ return issues
334
+
335
+
336
+ # ============================================================
337
+ # SVG Renderer
338
+ # ============================================================
339
+
340
+ def render_svg(spec: dict) -> str:
341
+ """Render FigureSpec to SVG string."""
342
+ canvas = spec.get("canvas", {})
343
+ width = canvas.get("width", 800)
344
+ height = canvas.get("height", 400)
345
+ style = {**DEFAULT_STYLE, **spec.get("style", {})}
346
+ palette = style["palette"]
347
+ base_fs = style["font_size"]
348
+
349
+ svg = Element("svg", {
350
+ "xmlns": "http://www.w3.org/2000/svg",
351
+ "viewBox": f"0 0 {width} {height}",
352
+ "width": str(width),
353
+ "height": str(height),
354
+ "font-family": sanitize_text(style["font_family"]),
355
+ })
356
+
357
+ # Background
358
+ SubElement(svg, "rect", {
359
+ "width": str(width), "height": str(height),
360
+ "fill": sanitize_color(style.get("bg_color", "#FFFFFF")),
361
+ })
362
+
363
+ # Defs: arrow markers
364
+ defs = SubElement(svg, "defs")
365
+ marker_colors = {"default": sanitize_color(DEFAULT_EDGE["color"])}
366
+ for i, c in enumerate(palette):
367
+ marker_colors[f"c{i}"] = sanitize_color(c)
368
+
369
+ for name, color in marker_colors.items():
370
+ marker = SubElement(defs, "marker", {
371
+ "id": f"arrow-{name}",
372
+ "markerWidth": str(ARROW_SIZE + 2),
373
+ "markerHeight": str(ARROW_SIZE + 2),
374
+ "refX": str(ARROW_SIZE),
375
+ "refY": str(ARROW_SIZE // 2),
376
+ "orient": "auto",
377
+ "markerUnits": "strokeWidth",
378
+ })
379
+ SubElement(marker, "polygon", {
380
+ "points": f"0 0, {ARROW_SIZE} {ARROW_SIZE // 2}, 0 {ARROW_SIZE}",
381
+ "fill": color,
382
+ })
383
+
384
+ # Build node lookup with defaults applied
385
+ node_map = {}
386
+ for i, node in enumerate(spec.get("nodes", [])):
387
+ n = {**DEFAULT_NODE, **node}
388
+ n["fill"] = sanitize_color(n.get("fill") or lighten_color(palette[i % len(palette)]))
389
+ n["stroke"] = sanitize_color(n.get("stroke") or palette[i % len(palette)])
390
+ n["text_color"] = sanitize_color(n.get("text_color", DEFAULT_NODE["text_color"]))
391
+ n["label"] = sanitize_text(n.get("label", ""))
392
+ if n.get("sublabel"):
393
+ n["sublabel"] = sanitize_text(n["sublabel"])
394
+ if n.get("shape") not in ALLOWED_SHAPES:
395
+ n["shape"] = "rounded"
396
+ node_map[n["id"]] = n
397
+
398
+ # --- Render groups (background layer) ---
399
+ for group in spec.get("groups", []):
400
+ gnodes = [node_map[nid] for nid in group.get("node_ids", []) if nid in node_map]
401
+ if not gnodes:
402
+ continue
403
+ pad = group.get("padding", 20)
404
+ def _node_extent(n):
405
+ w, h = n["width"], n["height"]
406
+ if n.get("shape") == "circle":
407
+ d = max(w, h)
408
+ return d, d
409
+ return w, h
410
+
411
+ min_x = min(n["x"] - _node_extent(n)[0] / 2 for n in gnodes) - pad
412
+ min_y = min(n["y"] - _node_extent(n)[1] / 2 for n in gnodes) - pad
413
+ max_x = max(n["x"] + _node_extent(n)[0] / 2 for n in gnodes) + pad
414
+ max_y = max(n["y"] + _node_extent(n)[1] / 2 for n in gnodes) + pad
415
+
416
+ SubElement(svg, "rect", {
417
+ "x": f"{min_x:.1f}", "y": f"{min_y:.1f}",
418
+ "width": f"{max_x - min_x:.1f}", "height": f"{max_y - min_y:.1f}",
419
+ "fill": sanitize_color(group.get("fill", "#F5F5F5")),
420
+ "stroke": sanitize_color(group.get("stroke", "#E0E0E0")),
421
+ "stroke-width": "1", "rx": "8",
422
+ })
423
+ if group.get("label"):
424
+ lbl = SubElement(svg, "text", {
425
+ "x": f"{min_x + 8:.1f}", "y": f"{min_y + 16:.1f}",
426
+ "font-size": str(base_fs - 2),
427
+ "fill": "#999999", "font-weight": "bold",
428
+ })
429
+ lbl.text = sanitize_text(group["label"])
430
+
431
+ # --- Render edges ---
432
+ for edge in spec.get("edges", []):
433
+ e = {**DEFAULT_EDGE, **edge}
434
+ src = node_map.get(e.get("from"))
435
+ dst = node_map.get(e.get("to"))
436
+ if not src or not dst:
437
+ continue
438
+
439
+ color = sanitize_color(e["color"])
440
+ e_style = e["style"] if e["style"] in ALLOWED_STYLES else "solid"
441
+
442
+ # Find matching arrow marker
443
+ marker_id = "arrow-default"
444
+ for ci, pc in enumerate(palette):
445
+ if color.lower() == pc.lower():
446
+ marker_id = f"arrow-c{ci}"
447
+ break
448
+
449
+ # Dash pattern
450
+ dash_map = {"solid": "", "dashed": "8,4", "dotted": "3,3"}
451
+ dash = dash_map.get(e_style, "")
452
+
453
+ # Self-loop (shape-aware: find top boundary)
454
+ if src["id"] == dst["id"]:
455
+ cx, cy = src["x"], src["y"]
456
+ r = SELF_LOOP_RADIUS
457
+ src_shape = src.get("shape", "rounded")
458
+ # Find top anchor point based on shape
459
+ top_x, top_y_pt = clip_to_shape(cx, cy, cx, cy - 100, src["width"], src["height"], src_shape)
460
+ path_d = (f"M {top_x - 10},{top_y_pt} "
461
+ f"C {cx - r},{top_y_pt - r * 1.5} {cx + r},{top_y_pt - r * 1.5} {top_x + 10},{top_y_pt}")
462
+ attrs = {
463
+ "d": path_d,
464
+ "stroke": color,
465
+ "stroke-width": str(e["thickness"]),
466
+ "fill": "none",
467
+ "marker-end": f"url(#{marker_id})",
468
+ }
469
+ if dash:
470
+ attrs["stroke-dasharray"] = dash
471
+ SubElement(svg, "path", attrs)
472
+ else:
473
+ # Shape-aware clipping
474
+ sx, sy = clip_to_shape(src["x"], src["y"], dst["x"], dst["y"],
475
+ src["width"], src["height"], src.get("shape", "rounded"))
476
+ dx, dy = clip_to_shape(dst["x"], dst["y"], src["x"], src["y"],
477
+ dst["width"], dst["height"], dst.get("shape", "rounded"))
478
+
479
+ if e.get("curve"):
480
+ # Quadratic bezier with midpoint offset
481
+ mx = (sx + dx) / 2
482
+ my = (sy + dy) / 2
483
+ # Offset perpendicular to the line
484
+ length = math.sqrt((dx - sx) ** 2 + (dy - sy) ** 2) or 1
485
+ offset = 30
486
+ nx = -(dy - sy) / length * offset
487
+ ny = (dx - sx) / length * offset
488
+ path_d = f"M {sx:.1f},{sy:.1f} Q {mx + nx:.1f},{my + ny:.1f} {dx:.1f},{dy:.1f}"
489
+ else:
490
+ path_d = f"M {sx:.1f},{sy:.1f} L {dx:.1f},{dy:.1f}"
491
+
492
+ attrs = {
493
+ "d": path_d,
494
+ "stroke": color,
495
+ "stroke-width": str(e["thickness"]),
496
+ "fill": "none",
497
+ "marker-end": f"url(#{marker_id})",
498
+ }
499
+ if dash:
500
+ attrs["stroke-dasharray"] = dash
501
+ SubElement(svg, "path", attrs)
502
+
503
+ # Edge label
504
+ if e.get("label"):
505
+ label_text = sanitize_text(e["label"])
506
+ if src["id"] == dst["id"]:
507
+ lx = src["x"]
508
+ src_shape = src.get("shape", "rounded")
509
+ _, top_pt = clip_to_shape(src["x"], src["y"], src["x"], src["y"] - 100,
510
+ src["width"], src["height"], src_shape)
511
+ ly = top_pt - SELF_LOOP_RADIUS * 1.2
512
+ elif e.get("curve"):
513
+ # Bezier midpoint at t=0.5: B(0.5) = (1-t)^2*P0 + 2(1-t)t*P1 + t^2*P2
514
+ mx_ctrl = (sx + dx) / 2
515
+ my_ctrl = (sy + dy) / 2
516
+ length = math.sqrt((dx - sx) ** 2 + (dy - sy) ** 2) or 1
517
+ offset = 30
518
+ nx_ctrl = -(dy - sy) / length * offset
519
+ ny_ctrl = (dx - sx) / length * offset
520
+ qx, qy = mx_ctrl + nx_ctrl, my_ctrl + ny_ctrl
521
+ lx = 0.25 * sx + 0.5 * qx + 0.25 * dx
522
+ ly = 0.25 * sy + 0.5 * qy + 0.25 * dy - 8
523
+ else:
524
+ lx = (sx + dx) / 2
525
+ ly = (sy + dy) / 2 - 8
526
+
527
+ tw = estimate_text_width(label_text, base_fs - 3) + 8
528
+ SubElement(svg, "rect", {
529
+ "x": f"{lx - tw / 2:.1f}", "y": f"{ly - 10:.1f}",
530
+ "width": f"{tw:.1f}", "height": "16",
531
+ "fill": "#FFFFFF", "rx": "3", "opacity": "0.85",
532
+ })
533
+ lbl = SubElement(svg, "text", {
534
+ "x": f"{lx:.1f}", "y": f"{ly + 2:.1f}",
535
+ "font-size": str(base_fs - 3),
536
+ "fill": "#777777", "text-anchor": "middle",
537
+ })
538
+ lbl.text = label_text
539
+
540
+ # --- Render nodes ---
541
+ for node in spec.get("nodes", []):
542
+ n = node_map[node["id"]]
543
+ x, y = n["x"], n["y"]
544
+ w, h = n["width"], n["height"]
545
+ left, top = x - w / 2, y - h / 2
546
+ shape = n.get("shape", "rounded")
547
+ fill = n["fill"]
548
+ stroke = n["stroke"]
549
+
550
+ if shape == "circle":
551
+ r = max(w, h) / 2
552
+ SubElement(svg, "circle", {
553
+ "cx": f"{x:.1f}", "cy": f"{y:.1f}", "r": f"{r:.1f}",
554
+ "fill": fill, "stroke": stroke, "stroke-width": "2",
555
+ })
556
+ elif shape == "ellipse":
557
+ SubElement(svg, "ellipse", {
558
+ "cx": f"{x:.1f}", "cy": f"{y:.1f}",
559
+ "rx": f"{w / 2:.1f}", "ry": f"{h / 2:.1f}",
560
+ "fill": fill, "stroke": stroke, "stroke-width": "2",
561
+ })
562
+ elif shape == "diamond":
563
+ points = (f"{x:.1f},{top:.1f} {x + w / 2:.1f},{y:.1f} "
564
+ f"{x:.1f},{top + h:.1f} {x - w / 2:.1f},{y:.1f}")
565
+ SubElement(svg, "polygon", {
566
+ "points": points,
567
+ "fill": fill, "stroke": stroke, "stroke-width": "2",
568
+ })
569
+ else:
570
+ rx = "8" if shape == "rounded" else "0"
571
+ SubElement(svg, "rect", {
572
+ "x": f"{left:.1f}", "y": f"{top:.1f}",
573
+ "width": f"{w:.1f}", "height": f"{h:.1f}",
574
+ "fill": fill, "stroke": stroke,
575
+ "stroke-width": "2", "rx": rx,
576
+ })
577
+
578
+ # Main label (supports \n for multi-line)
579
+ fs = n.get("font_size") or base_fs
580
+ # Handle multi-line: first replace literal \\n with \n, then split
581
+ raw_label = n["label"].replace("\\n", "\n")
582
+ label_lines = [line for line in raw_label.split("\n") if line]
583
+ has_sub = bool(n.get("sublabel"))
584
+ total_lines = len(label_lines) + (1 if has_sub else 0)
585
+ line_height = fs + 2
586
+ start_y = y - (total_lines - 1) * line_height / 2 + fs * 0.35
587
+
588
+ for li, line in enumerate(label_lines):
589
+ lbl = SubElement(svg, "text", {
590
+ "x": f"{x:.1f}",
591
+ "y": f"{start_y + li * line_height:.1f}",
592
+ "font-size": str(fs),
593
+ "fill": n["text_color"],
594
+ "text-anchor": "middle",
595
+ "font-weight": "bold",
596
+ })
597
+ lbl.text = line
598
+
599
+ # Sublabel
600
+ if has_sub:
601
+ sub = SubElement(svg, "text", {
602
+ "x": f"{x:.1f}",
603
+ "y": f"{start_y + len(label_lines) * line_height:.1f}",
604
+ "font-size": str(fs - 3),
605
+ "fill": "#888888",
606
+ "text-anchor": "middle",
607
+ })
608
+ sub.text = sanitize_text(n["sublabel"])
609
+
610
+ # --- Free labels ---
611
+ for label in spec.get("labels", []):
612
+ anchor = label.get("anchor", "middle")
613
+ if anchor not in ALLOWED_ANCHORS:
614
+ anchor = "middle"
615
+ lbl = SubElement(svg, "text", {
616
+ "x": f"{label.get('x', 0):.1f}",
617
+ "y": f"{label.get('y', 0):.1f}",
618
+ "font-size": str(label.get("font_size", base_fs)),
619
+ "fill": sanitize_color(label.get("color", "#555555")),
620
+ "text-anchor": anchor,
621
+ })
622
+ lbl.text = sanitize_text(label.get("text", ""))
623
+
624
+ # Pretty print
625
+ raw = tostring(svg, encoding="unicode")
626
+ pretty = parseString(raw).toprettyxml(indent=" ")
627
+ # Remove xml declaration line
628
+ lines = pretty.split("\n")
629
+ return "\n".join(lines[1:])
630
+
631
+
632
+ # ============================================================
633
+ # PNG Preview
634
+ # ============================================================
635
+
636
+ def svg_to_png(svg_path: str, png_path: str) -> bool:
637
+ """Convert SVG to PNG preview."""
638
+ import subprocess
639
+
640
+ # Try rsvg-convert
641
+ try:
642
+ result = subprocess.run(
643
+ ["rsvg-convert", "-o", png_path, svg_path],
644
+ capture_output=True, timeout=30
645
+ )
646
+ if result.returncode == 0:
647
+ return True
648
+ except (FileNotFoundError, subprocess.TimeoutExpired):
649
+ pass
650
+
651
+ # Try cairosvg
652
+ try:
653
+ os.environ.setdefault("DYLD_LIBRARY_PATH", "/opt/homebrew/lib")
654
+ import cairosvg
655
+ cairosvg.svg2png(url=svg_path, write_to=png_path)
656
+ return True
657
+ except Exception:
658
+ pass
659
+
660
+ print("Warning: could not convert SVG to PNG (install rsvg-convert or cairosvg)")
661
+ return False
662
+
663
+
664
+ # ============================================================
665
+ # Schema (for documentation)
666
+ # ============================================================
667
+
668
+ SCHEMA_DOC = """\
669
+ FigureSpec JSON Schema:
670
+ {
671
+ "title": "string — figure title (metadata only, not rendered)",
672
+ "canvas": {"width": int, "height": int},
673
+ "style": {
674
+ "font_family": "CSS font string",
675
+ "font_size": int (default 14),
676
+ "bg_color": "#RRGGBB",
677
+ "palette": ["#color1", "#color2", ...]
678
+ },
679
+ "nodes": [{
680
+ "id": "string (required, unique)",
681
+ "label": "string (required, supports \\\\n for multi-line)",
682
+ "x": int (required, center x),
683
+ "y": int (required, center y),
684
+ "width": int (default 120),
685
+ "height": int (default 50),
686
+ "shape": "rounded | rect | circle | ellipse | diamond",
687
+ "fill": "#RRGGBB (auto from palette)",
688
+ "stroke": "#RRGGBB (auto from palette)",
689
+ "text_color": "#RRGGBB (default #333333)",
690
+ "font_size": int (override),
691
+ "sublabel": "string (smaller text below label)"
692
+ }],
693
+ "edges": [{
694
+ "from": "node_id (required)",
695
+ "to": "node_id (required, same as from = self-loop)",
696
+ "label": "string",
697
+ "style": "solid | dashed | dotted",
698
+ "color": "#RRGGBB (default #555555)",
699
+ "thickness": int (default 2),
700
+ "curve": bool (default false)
701
+ }],
702
+ "groups": [{
703
+ "id": "string",
704
+ "label": "string",
705
+ "node_ids": ["id1", "id2"],
706
+ "fill": "#RRGGBB",
707
+ "stroke": "#RRGGBB",
708
+ "padding": int (default 20)
709
+ }],
710
+ "labels": [{
711
+ "text": "string",
712
+ "x": int, "y": int,
713
+ "font_size": int,
714
+ "color": "#RRGGBB",
715
+ "anchor": "start | middle | end"
716
+ }]
717
+ }
718
+ """
719
+
720
+
721
+ # ============================================================
722
+ # CLI
723
+ # ============================================================
724
+
725
+ def main():
726
+ parser = argparse.ArgumentParser(description="ARIS FigureSpec → SVG Renderer")
727
+ subparsers = parser.add_subparsers(dest="command")
728
+
729
+ p_render = subparsers.add_parser("render", help="Render FigureSpec JSON to SVG")
730
+ p_render.add_argument("spec_file", help="FigureSpec JSON file")
731
+ p_render.add_argument("--output", "-o", default=None, help="Output SVG path")
732
+ p_render.add_argument("--preview", action="store_true", help="Also generate PNG preview")
733
+
734
+ p_validate = subparsers.add_parser("validate", help="Validate FigureSpec JSON")
735
+ p_validate.add_argument("spec_file", help="FigureSpec JSON file")
736
+
737
+ subparsers.add_parser("schema", help="Print FigureSpec schema documentation")
738
+
739
+ args = parser.parse_args()
740
+
741
+ if args.command == "schema":
742
+ print(SCHEMA_DOC)
743
+ return
744
+
745
+ if args.command == "validate":
746
+ with open(args.spec_file, encoding="utf-8") as f:
747
+ spec = json.load(f)
748
+ issues = validate_spec(spec)
749
+ critical = sum(1 for i in issues if i.startswith("CRITICAL"))
750
+ if not issues:
751
+ print("✅ FigureSpec is valid")
752
+ else:
753
+ for issue in issues:
754
+ print(f" {issue}")
755
+ print(f"\n{len(issues)} issues ({critical} critical)")
756
+ sys.exit(1 if critical else 0)
757
+
758
+ if args.command == "render":
759
+ with open(args.spec_file, encoding="utf-8") as f:
760
+ spec = json.load(f)
761
+
762
+ issues = validate_spec(spec)
763
+ critical = [i for i in issues if i.startswith("CRITICAL")]
764
+ if critical:
765
+ print("❌ Cannot render — critical issues:")
766
+ for i in critical:
767
+ print(f" {i}")
768
+ sys.exit(1)
769
+
770
+ if issues:
771
+ print(f"⚠️ {len(issues)} warnings:")
772
+ for i in issues:
773
+ print(f" {i}")
774
+
775
+ svg_content = render_svg(spec)
776
+
777
+ # Output path
778
+ if args.output:
779
+ output = args.output
780
+ else:
781
+ base = Path(args.spec_file).stem
782
+ output = str(Path(args.spec_file).parent / f"{base}.svg")
783
+
784
+ Path(output).parent.mkdir(parents=True, exist_ok=True)
785
+ with open(output, "w", encoding="utf-8") as f:
786
+ f.write(svg_content)
787
+ print(f"✅ SVG written: {output}")
788
+
789
+ if args.preview:
790
+ png_path = str(Path(output).with_suffix(".png"))
791
+ if svg_to_png(output, png_path):
792
+ print(f"✅ PNG preview: {png_path}")
793
+ return
794
+
795
+ parser.print_help()
796
+
797
+
798
+ if __name__ == "__main__":
799
+ main()