kingmadoc 0.3.0.dev8__tar.gz → 0.3.0.dev9__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (172) hide show
  1. {kingmadoc-0.3.0.dev8 → kingmadoc-0.3.0.dev9}/CHANGELOG.md +8 -0
  2. {kingmadoc-0.3.0.dev8 → kingmadoc-0.3.0.dev9}/CLAUDE.md +1 -1
  3. {kingmadoc-0.3.0.dev8 → kingmadoc-0.3.0.dev9}/PKG-INFO +8 -9
  4. {kingmadoc-0.3.0.dev8 → kingmadoc-0.3.0.dev9}/README.md +6 -8
  5. {kingmadoc-0.3.0.dev8 → kingmadoc-0.3.0.dev9}/pyproject.toml +7 -1
  6. {kingmadoc-0.3.0.dev8 → kingmadoc-0.3.0.dev9}/skill/explaining-code/SKILL.md +9 -7
  7. {kingmadoc-0.3.0.dev8 → kingmadoc-0.3.0.dev9}/skill/explaining-code/reference/arc42.md +1 -1
  8. {kingmadoc-0.3.0.dev8 → kingmadoc-0.3.0.dev9}/skill/explaining-code/reference/c4.md +1 -1
  9. {kingmadoc-0.3.0.dev8 → kingmadoc-0.3.0.dev9}/skill/explaining-code/reference/split.md +1 -1
  10. {kingmadoc-0.3.0.dev8 → kingmadoc-0.3.0.dev9}/src/kingmadoc/cli.py +51 -21
  11. kingmadoc-0.3.0.dev9/src/kingmadoc/raster.py +141 -0
  12. {kingmadoc-0.3.0.dev8 → kingmadoc-0.3.0.dev9}/src/kingmadoc/render.py +32 -12
  13. {kingmadoc-0.3.0.dev8 → kingmadoc-0.3.0.dev9}/src/kingmadoc/vscode.py +41 -4
  14. kingmadoc-0.3.0.dev9/tests/conftest.py +14 -0
  15. kingmadoc-0.3.0.dev9/tests/data/d2-sample.svg +51 -0
  16. kingmadoc-0.3.0.dev9/tests/test_raster.py +96 -0
  17. {kingmadoc-0.3.0.dev8 → kingmadoc-0.3.0.dev9}/tests/test_render.py +60 -26
  18. {kingmadoc-0.3.0.dev8 → kingmadoc-0.3.0.dev9}/tests/test_vscode_preview.py +69 -6
  19. {kingmadoc-0.3.0.dev8 → kingmadoc-0.3.0.dev9}/uv.lock +67 -0
  20. {kingmadoc-0.3.0.dev8 → kingmadoc-0.3.0.dev9}/.featuredoc.yml +0 -0
  21. {kingmadoc-0.3.0.dev8 → kingmadoc-0.3.0.dev9}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
  22. {kingmadoc-0.3.0.dev8 → kingmadoc-0.3.0.dev9}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
  23. {kingmadoc-0.3.0.dev8 → kingmadoc-0.3.0.dev9}/.github/workflows/ci.yml +0 -0
  24. {kingmadoc-0.3.0.dev8 → kingmadoc-0.3.0.dev9}/.github/workflows/release.yml +0 -0
  25. {kingmadoc-0.3.0.dev8 → kingmadoc-0.3.0.dev9}/.gitignore +0 -0
  26. {kingmadoc-0.3.0.dev8 → kingmadoc-0.3.0.dev9}/CONTRIBUTING.md +0 -0
  27. {kingmadoc-0.3.0.dev8 → kingmadoc-0.3.0.dev9}/LICENSE +0 -0
  28. {kingmadoc-0.3.0.dev8 → kingmadoc-0.3.0.dev9}/docs/conventions.md +0 -0
  29. {kingmadoc-0.3.0.dev8 → kingmadoc-0.3.0.dev9}/docs/index.md +0 -0
  30. {kingmadoc-0.3.0.dev8 → kingmadoc-0.3.0.dev9}/docs/releasing.md +0 -0
  31. {kingmadoc-0.3.0.dev8 → kingmadoc-0.3.0.dev9}/docs/roadmap.md +0 -0
  32. {kingmadoc-0.3.0.dev8 → kingmadoc-0.3.0.dev9}/docs/test-plan.md +0 -0
  33. {kingmadoc-0.3.0.dev8 → kingmadoc-0.3.0.dev9}/evals/README.md +0 -0
  34. {kingmadoc-0.3.0.dev8 → kingmadoc-0.3.0.dev9}/evals/fixtures/shop/manage.py +0 -0
  35. {kingmadoc-0.3.0.dev8 → kingmadoc-0.3.0.dev9}/evals/fixtures/shop/requirements.txt +0 -0
  36. {kingmadoc-0.3.0.dev8 → kingmadoc-0.3.0.dev9}/evals/fixtures/shop/shop/__init__.py +0 -0
  37. {kingmadoc-0.3.0.dev8 → kingmadoc-0.3.0.dev9}/evals/fixtures/shop/shop/models.py +0 -0
  38. {kingmadoc-0.3.0.dev8 → kingmadoc-0.3.0.dev9}/evals/fixtures/shop/shop/services.py +0 -0
  39. {kingmadoc-0.3.0.dev8 → kingmadoc-0.3.0.dev9}/evals/fixtures/shop/shop/settings.py +0 -0
  40. {kingmadoc-0.3.0.dev8 → kingmadoc-0.3.0.dev9}/evals/fixtures/shop/shop/urls.py +0 -0
  41. {kingmadoc-0.3.0.dev8 → kingmadoc-0.3.0.dev9}/evals/fixtures/shop/shop/views.py +0 -0
  42. {kingmadoc-0.3.0.dev8 → kingmadoc-0.3.0.dev9}/evals/fixtures/shop-discount/shop/discounts.py +0 -0
  43. {kingmadoc-0.3.0.dev8 → kingmadoc-0.3.0.dev9}/evals/fixtures/shop-discount/shop/services.py +0 -0
  44. {kingmadoc-0.3.0.dev8 → kingmadoc-0.3.0.dev9}/evals/results/2026-09-27T111837Z.json +0 -0
  45. {kingmadoc-0.3.0.dev8 → kingmadoc-0.3.0.dev9}/evals/results/2026-09-27T112600Z.json +0 -0
  46. {kingmadoc-0.3.0.dev8 → kingmadoc-0.3.0.dev9}/evals/results/2026-09-27T162715Z.json +0 -0
  47. {kingmadoc-0.3.0.dev8 → kingmadoc-0.3.0.dev9}/evals/scenarios/explain-branch.yml +0 -0
  48. {kingmadoc-0.3.0.dev8 → kingmadoc-0.3.0.dev9}/evals/scenarios/explain-feature.yml +0 -0
  49. {kingmadoc-0.3.0.dev8 → kingmadoc-0.3.0.dev9}/evals/scenarios/plan-feature.yml +0 -0
  50. {kingmadoc-0.3.0.dev8 → kingmadoc-0.3.0.dev9}/examples/verify-mode-plan.md +0 -0
  51. {kingmadoc-0.3.0.dev8 → kingmadoc-0.3.0.dev9}/scripts/build_skill_variants.py +0 -0
  52. {kingmadoc-0.3.0.dev8 → kingmadoc-0.3.0.dev9}/scripts/run_evals.py +0 -0
  53. {kingmadoc-0.3.0.dev8 → kingmadoc-0.3.0.dev9}/skill/SKILL.md +0 -0
  54. {kingmadoc-0.3.0.dev8 → kingmadoc-0.3.0.dev9}/skill/codex.md +0 -0
  55. {kingmadoc-0.3.0.dev8 → kingmadoc-0.3.0.dev9}/skill/copilot.md +0 -0
  56. {kingmadoc-0.3.0.dev8 → kingmadoc-0.3.0.dev9}/skill/cursor.md +0 -0
  57. {kingmadoc-0.3.0.dev8 → kingmadoc-0.3.0.dev9}/skill/explaining-code/reference/c4-model.md +0 -0
  58. {kingmadoc-0.3.0.dev8 → kingmadoc-0.3.0.dev9}/skill/explaining-code/reference/models.md +0 -0
  59. {kingmadoc-0.3.0.dev8 → kingmadoc-0.3.0.dev9}/skill/reference/diagram-rules.md +0 -0
  60. {kingmadoc-0.3.0.dev8 → kingmadoc-0.3.0.dev9}/skill/reference/formats.md +0 -0
  61. {kingmadoc-0.3.0.dev8 → kingmadoc-0.3.0.dev9}/src/kingmadoc/__init__.py +0 -0
  62. {kingmadoc-0.3.0.dev8 → kingmadoc-0.3.0.dev9}/src/kingmadoc/about.py +0 -0
  63. {kingmadoc-0.3.0.dev8 → kingmadoc-0.3.0.dev9}/src/kingmadoc/adr.py +0 -0
  64. {kingmadoc-0.3.0.dev8 → kingmadoc-0.3.0.dev9}/src/kingmadoc/config.py +0 -0
  65. {kingmadoc-0.3.0.dev8 → kingmadoc-0.3.0.dev9}/src/kingmadoc/d2_binary.py +0 -0
  66. {kingmadoc-0.3.0.dev8 → kingmadoc-0.3.0.dev9}/src/kingmadoc/diagrams/__init__.py +0 -0
  67. {kingmadoc-0.3.0.dev8 → kingmadoc-0.3.0.dev9}/src/kingmadoc/diagrams/base.py +0 -0
  68. {kingmadoc-0.3.0.dev8 → kingmadoc-0.3.0.dev9}/src/kingmadoc/diagrams/d2.py +0 -0
  69. {kingmadoc-0.3.0.dev8 → kingmadoc-0.3.0.dev9}/src/kingmadoc/diagrams/mermaid.py +0 -0
  70. {kingmadoc-0.3.0.dev8 → kingmadoc-0.3.0.dev9}/src/kingmadoc/diagrams/plantuml.py +0 -0
  71. {kingmadoc-0.3.0.dev8 → kingmadoc-0.3.0.dev9}/src/kingmadoc/documents.py +0 -0
  72. {kingmadoc-0.3.0.dev8 → kingmadoc-0.3.0.dev9}/src/kingmadoc/exceptions.py +0 -0
  73. {kingmadoc-0.3.0.dev8 → kingmadoc-0.3.0.dev9}/src/kingmadoc/explain.py +0 -0
  74. {kingmadoc-0.3.0.dev8 → kingmadoc-0.3.0.dev9}/src/kingmadoc/facts/__init__.py +0 -0
  75. {kingmadoc-0.3.0.dev8 → kingmadoc-0.3.0.dev9}/src/kingmadoc/facts/branch.py +0 -0
  76. {kingmadoc-0.3.0.dev8 → kingmadoc-0.3.0.dev9}/src/kingmadoc/facts/collect.py +0 -0
  77. {kingmadoc-0.3.0.dev8 → kingmadoc-0.3.0.dev9}/src/kingmadoc/facts/data_model.py +0 -0
  78. {kingmadoc-0.3.0.dev8 → kingmadoc-0.3.0.dev9}/src/kingmadoc/facts/dominators.py +0 -0
  79. {kingmadoc-0.3.0.dev8 → kingmadoc-0.3.0.dev9}/src/kingmadoc/facts/js_modules.py +0 -0
  80. {kingmadoc-0.3.0.dev8 → kingmadoc-0.3.0.dev9}/src/kingmadoc/facts/projects.py +0 -0
  81. {kingmadoc-0.3.0.dev8 → kingmadoc-0.3.0.dev9}/src/kingmadoc/facts/routes.py +0 -0
  82. {kingmadoc-0.3.0.dev8 → kingmadoc-0.3.0.dev9}/src/kingmadoc/facts/services.py +0 -0
  83. {kingmadoc-0.3.0.dev8 → kingmadoc-0.3.0.dev9}/src/kingmadoc/git.py +0 -0
  84. {kingmadoc-0.3.0.dev8 → kingmadoc-0.3.0.dev9}/src/kingmadoc/naming.py +0 -0
  85. {kingmadoc-0.3.0.dev8 → kingmadoc-0.3.0.dev9}/src/kingmadoc/plan/__init__.py +0 -0
  86. {kingmadoc-0.3.0.dev8 → kingmadoc-0.3.0.dev9}/src/kingmadoc/plan/analyzer.py +0 -0
  87. {kingmadoc-0.3.0.dev8 → kingmadoc-0.3.0.dev9}/src/kingmadoc/plan/dependencies.py +0 -0
  88. {kingmadoc-0.3.0.dev8 → kingmadoc-0.3.0.dev9}/src/kingmadoc/plan/generator.py +0 -0
  89. {kingmadoc-0.3.0.dev8 → kingmadoc-0.3.0.dev9}/src/kingmadoc/plan/models.py +0 -0
  90. {kingmadoc-0.3.0.dev8 → kingmadoc-0.3.0.dev9}/src/kingmadoc/plandoc.py +0 -0
  91. {kingmadoc-0.3.0.dev8 → kingmadoc-0.3.0.dev9}/src/kingmadoc/skills.py +0 -0
  92. {kingmadoc-0.3.0.dev8 → kingmadoc-0.3.0.dev9}/src/kingmadoc/templates/adr.md.j2 +0 -0
  93. {kingmadoc-0.3.0.dev8 → kingmadoc-0.3.0.dev9}/src/kingmadoc/templates/domain_design.md.j2 +0 -0
  94. {kingmadoc-0.3.0.dev8 → kingmadoc-0.3.0.dev9}/src/kingmadoc/templates/functional_design.md.j2 +0 -0
  95. {kingmadoc-0.3.0.dev8 → kingmadoc-0.3.0.dev9}/src/kingmadoc/templates/plan_default.md.j2 +0 -0
  96. {kingmadoc-0.3.0.dev8 → kingmadoc-0.3.0.dev9}/src/kingmadoc/templates/security_design.md.j2 +0 -0
  97. {kingmadoc-0.3.0.dev8 → kingmadoc-0.3.0.dev9}/src/kingmadoc/templates/technical_design.md.j2 +0 -0
  98. {kingmadoc-0.3.0.dev8 → kingmadoc-0.3.0.dev9}/src/kingmadoc/templating.py +0 -0
  99. {kingmadoc-0.3.0.dev8 → kingmadoc-0.3.0.dev9}/src/kingmadoc/verify/__init__.py +0 -0
  100. {kingmadoc-0.3.0.dev8 → kingmadoc-0.3.0.dev9}/src/kingmadoc/verify/changes.py +0 -0
  101. {kingmadoc-0.3.0.dev8 → kingmadoc-0.3.0.dev9}/src/kingmadoc/verify/commands.py +0 -0
  102. {kingmadoc-0.3.0.dev8 → kingmadoc-0.3.0.dev9}/src/kingmadoc/verify/deviations.py +0 -0
  103. {kingmadoc-0.3.0.dev8 → kingmadoc-0.3.0.dev9}/src/kingmadoc/verify/locate.py +0 -0
  104. {kingmadoc-0.3.0.dev8 → kingmadoc-0.3.0.dev9}/src/kingmadoc/verify/report.py +0 -0
  105. {kingmadoc-0.3.0.dev8 → kingmadoc-0.3.0.dev9}/tests/fixtures/backends/d2/class.d2 +0 -0
  106. {kingmadoc-0.3.0.dev8 → kingmadoc-0.3.0.dev9}/tests/fixtures/backends/d2/component.d2 +0 -0
  107. {kingmadoc-0.3.0.dev8 → kingmadoc-0.3.0.dev9}/tests/fixtures/backends/d2/container.d2 +0 -0
  108. {kingmadoc-0.3.0.dev8 → kingmadoc-0.3.0.dev9}/tests/fixtures/backends/d2/context.d2 +0 -0
  109. {kingmadoc-0.3.0.dev8 → kingmadoc-0.3.0.dev9}/tests/fixtures/backends/d2/sequence.d2 +0 -0
  110. {kingmadoc-0.3.0.dev8 → kingmadoc-0.3.0.dev9}/tests/fixtures/backends/mermaid/class.mmd +0 -0
  111. {kingmadoc-0.3.0.dev8 → kingmadoc-0.3.0.dev9}/tests/fixtures/backends/mermaid/component.mmd +0 -0
  112. {kingmadoc-0.3.0.dev8 → kingmadoc-0.3.0.dev9}/tests/fixtures/backends/mermaid/container.mmd +0 -0
  113. {kingmadoc-0.3.0.dev8 → kingmadoc-0.3.0.dev9}/tests/fixtures/backends/mermaid/context.mmd +0 -0
  114. {kingmadoc-0.3.0.dev8 → kingmadoc-0.3.0.dev9}/tests/fixtures/backends/mermaid/sequence.mmd +0 -0
  115. {kingmadoc-0.3.0.dev8 → kingmadoc-0.3.0.dev9}/tests/fixtures/backends/plantuml/class.puml +0 -0
  116. {kingmadoc-0.3.0.dev8 → kingmadoc-0.3.0.dev9}/tests/fixtures/backends/plantuml/component.puml +0 -0
  117. {kingmadoc-0.3.0.dev8 → kingmadoc-0.3.0.dev9}/tests/fixtures/backends/plantuml/container.puml +0 -0
  118. {kingmadoc-0.3.0.dev8 → kingmadoc-0.3.0.dev9}/tests/fixtures/backends/plantuml/context.puml +0 -0
  119. {kingmadoc-0.3.0.dev8 → kingmadoc-0.3.0.dev9}/tests/fixtures/backends/plantuml/sequence.puml +0 -0
  120. {kingmadoc-0.3.0.dev8 → kingmadoc-0.3.0.dev9}/tests/fixtures/mermaid/component.mmd +0 -0
  121. {kingmadoc-0.3.0.dev8 → kingmadoc-0.3.0.dev9}/tests/fixtures/mermaid/container.mmd +0 -0
  122. {kingmadoc-0.3.0.dev8 → kingmadoc-0.3.0.dev9}/tests/fixtures/mermaid/context.mmd +0 -0
  123. {kingmadoc-0.3.0.dev8 → kingmadoc-0.3.0.dev9}/tests/test_adr.py +0 -0
  124. {kingmadoc-0.3.0.dev8 → kingmadoc-0.3.0.dev9}/tests/test_adr_numbering.py +0 -0
  125. {kingmadoc-0.3.0.dev8 → kingmadoc-0.3.0.dev9}/tests/test_analyzer.py +0 -0
  126. {kingmadoc-0.3.0.dev8 → kingmadoc-0.3.0.dev9}/tests/test_cli.py +0 -0
  127. {kingmadoc-0.3.0.dev8 → kingmadoc-0.3.0.dev9}/tests/test_cli_encoding.py +0 -0
  128. {kingmadoc-0.3.0.dev8 → kingmadoc-0.3.0.dev9}/tests/test_cli_init.py +0 -0
  129. {kingmadoc-0.3.0.dev8 → kingmadoc-0.3.0.dev9}/tests/test_cli_plan_custom_template.py +0 -0
  130. {kingmadoc-0.3.0.dev8 → kingmadoc-0.3.0.dev9}/tests/test_cli_sigint.py +0 -0
  131. {kingmadoc-0.3.0.dev8 → kingmadoc-0.3.0.dev9}/tests/test_cli_verify_config.py +0 -0
  132. {kingmadoc-0.3.0.dev8 → kingmadoc-0.3.0.dev9}/tests/test_config.py +0 -0
  133. {kingmadoc-0.3.0.dev8 → kingmadoc-0.3.0.dev9}/tests/test_config_poetry.py +0 -0
  134. {kingmadoc-0.3.0.dev8 → kingmadoc-0.3.0.dev9}/tests/test_config_shape.py +0 -0
  135. {kingmadoc-0.3.0.dev8 → kingmadoc-0.3.0.dev9}/tests/test_d2_download.py +0 -0
  136. {kingmadoc-0.3.0.dev8 → kingmadoc-0.3.0.dev9}/tests/test_dependencies.py +0 -0
  137. {kingmadoc-0.3.0.dev8 → kingmadoc-0.3.0.dev9}/tests/test_dependency_graph_model.py +0 -0
  138. {kingmadoc-0.3.0.dev8 → kingmadoc-0.3.0.dev9}/tests/test_design_models.py +0 -0
  139. {kingmadoc-0.3.0.dev8 → kingmadoc-0.3.0.dev9}/tests/test_diagram_backends.py +0 -0
  140. {kingmadoc-0.3.0.dev8 → kingmadoc-0.3.0.dev9}/tests/test_diagrams_mermaid.py +0 -0
  141. {kingmadoc-0.3.0.dev8 → kingmadoc-0.3.0.dev9}/tests/test_documents.py +0 -0
  142. {kingmadoc-0.3.0.dev8 → kingmadoc-0.3.0.dev9}/tests/test_duplicate_names.py +0 -0
  143. {kingmadoc-0.3.0.dev8 → kingmadoc-0.3.0.dev9}/tests/test_evals.py +0 -0
  144. {kingmadoc-0.3.0.dev8 → kingmadoc-0.3.0.dev9}/tests/test_explain.py +0 -0
  145. {kingmadoc-0.3.0.dev8 → kingmadoc-0.3.0.dev9}/tests/test_explain_config.py +0 -0
  146. {kingmadoc-0.3.0.dev8 → kingmadoc-0.3.0.dev9}/tests/test_explain_status.py +0 -0
  147. {kingmadoc-0.3.0.dev8 → kingmadoc-0.3.0.dev9}/tests/test_extra_designs_coverage.py +0 -0
  148. {kingmadoc-0.3.0.dev8 → kingmadoc-0.3.0.dev9}/tests/test_facts.py +0 -0
  149. {kingmadoc-0.3.0.dev8 → kingmadoc-0.3.0.dev9}/tests/test_facts_code.py +0 -0
  150. {kingmadoc-0.3.0.dev8 → kingmadoc-0.3.0.dev9}/tests/test_facts_data_model.py +0 -0
  151. {kingmadoc-0.3.0.dev8 → kingmadoc-0.3.0.dev9}/tests/test_facts_dominators.py +0 -0
  152. {kingmadoc-0.3.0.dev8 → kingmadoc-0.3.0.dev9}/tests/test_functional_design.py +0 -0
  153. {kingmadoc-0.3.0.dev8 → kingmadoc-0.3.0.dev9}/tests/test_generator.py +0 -0
  154. {kingmadoc-0.3.0.dev8 → kingmadoc-0.3.0.dev9}/tests/test_grep_performance.py +0 -0
  155. {kingmadoc-0.3.0.dev8 → kingmadoc-0.3.0.dev9}/tests/test_manifests.py +0 -0
  156. {kingmadoc-0.3.0.dev8 → kingmadoc-0.3.0.dev9}/tests/test_max_lines_per_file.py +0 -0
  157. {kingmadoc-0.3.0.dev8 → kingmadoc-0.3.0.dev9}/tests/test_output_dir.py +0 -0
  158. {kingmadoc-0.3.0.dev8 → kingmadoc-0.3.0.dev9}/tests/test_plan_e2e.py +0 -0
  159. {kingmadoc-0.3.0.dev8 → kingmadoc-0.3.0.dev9}/tests/test_plandoc.py +0 -0
  160. {kingmadoc-0.3.0.dev8 → kingmadoc-0.3.0.dev9}/tests/test_properties.py +0 -0
  161. {kingmadoc-0.3.0.dev8 → kingmadoc-0.3.0.dev9}/tests/test_security_domain_designs.py +0 -0
  162. {kingmadoc-0.3.0.dev8 → kingmadoc-0.3.0.dev9}/tests/test_skill.py +0 -0
  163. {kingmadoc-0.3.0.dev8 → kingmadoc-0.3.0.dev9}/tests/test_skill_explaining_code.py +0 -0
  164. {kingmadoc-0.3.0.dev8 → kingmadoc-0.3.0.dev9}/tests/test_skill_models.py +0 -0
  165. {kingmadoc-0.3.0.dev8 → kingmadoc-0.3.0.dev9}/tests/test_skills_install.py +0 -0
  166. {kingmadoc-0.3.0.dev8 → kingmadoc-0.3.0.dev9}/tests/test_source_dirs.py +0 -0
  167. {kingmadoc-0.3.0.dev8 → kingmadoc-0.3.0.dev9}/tests/test_summary_slug_diagram_defaults.py +0 -0
  168. {kingmadoc-0.3.0.dev8 → kingmadoc-0.3.0.dev9}/tests/test_technical_design.py +0 -0
  169. {kingmadoc-0.3.0.dev8 → kingmadoc-0.3.0.dev9}/tests/test_templating_security.py +0 -0
  170. {kingmadoc-0.3.0.dev8 → kingmadoc-0.3.0.dev9}/tests/test_verify.py +0 -0
  171. {kingmadoc-0.3.0.dev8 → kingmadoc-0.3.0.dev9}/tests/test_verify_locate.py +0 -0
  172. {kingmadoc-0.3.0.dev8 → kingmadoc-0.3.0.dev9}/tests/test_version.py +0 -0
@@ -9,6 +9,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
9
9
 
10
10
  ### Changed
11
11
 
12
+ - `kingmadoc render` links a PNG for each diagram, so the pictures show in every
13
+ Markdown preview (VS Code, Visual Studio, Rider, GitHub, GitLab, Bitbucket; several
14
+ block or mishandle SVG). The PNG is made offline with resvg (`resvg-py`, a small
15
+ wheel for every platform) using D2's own embedded fonts; the SVG (dark mode) and the
16
+ `.d2` source stay next to it, and `--format svg` links the SVG as before.
17
+ `explaining-code` 5.8 no longer lets the agent convert images itself.
18
+ - `kingmadoc skills install --vscode-user`: VS Code opens explainers as a preview in
19
+ every folder (user settings); the question in a terminal sets that up.
12
20
  - Tidier diagrams: `kingmadoc render` lays figures out with ELK (straight,
13
21
  right-angled arrows with fewer crossings; a diagram that sets its own
14
22
  `layout-engine` keeps it) and warns about more than 12 arrows or two arrows between
@@ -33,7 +33,7 @@ Flow for `plan "<description>"` (`cli.py`): `config.load_config` → `plan.analy
33
33
  - `plan/analyzer.py`: pathlib walk with fnmatch excludes ("glob", hard cap `MAX_FILES_LIMIT` = 5000), manifest parsing (`pyproject.toml`, `requirements.txt`, `package.json`, `go.mod`, `Cargo.toml`, compose files → `DEPENDENCY_TECH`), regex import scanning ("grep"). Returns an immutable `CodebaseReport` (`kingmadoc analyze` prints it via `format_report`, `--json` via `report_to_dict`); `source_dirs` drives container inference (descends into `src/` layouts, skips tests/docs/examples).
34
34
  - `skill/SKILL.md`: the product skill (Markdown-only KingmaDoc for users without Python; not the repo's `checking-conventions` dev skill): the workflow, under 250 lines. Its document formats live in `skill/reference/formats.md` and must keep the same headings as `src/kingmadoc/templates/plan_default.md.j2`, the extra-design templates and `verify/report.py` — `tests/test_skill.py` fails otherwise, so change both together; the Mermaid rules in `skill/reference/diagram-rules.md`. SKILL.md keeps a `## Output format` / `## Diagram rules` stub that links each reference. `skill/cursor.md`, `codex.md`, `copilot.md` are single files generated by `scripts/build_skill_variants.py`, which inlines each reference in place of its stub — never edit them; rerun the script after changing SKILL.md or a reference (`tests/test_skill.py` fails on stale variants). `kingmadoc skills install` copies SKILL.md plus `reference/`.
35
35
  - `adr.py` (`kingmadoc adr "<title>"`, gated by the top-level `adr.enabled`; `adr.template` selects the template): `adr_path` → `next_number` (max existing + 1, never reused) + `adr_filename` → `render_adr` (`templates/adr.md.j2`, bundled) → `docs/adr/<NNNN>-<slug>.md`. Not a mode; shares `naming.slugify` and `templating.load_template` with plan.
36
- - `render.py` (`kingmadoc render <doc>`): renders each ```` ```d2 ```` block to `img/<doc>-<n>.svg` (D2 via `d2_binary.ensure_d2`), moves its source to `img/<doc>-<n>.d2`, and leaves only `<!-- kingmadoc:diagram img/<doc>-<n>.d2 -->` + the image in the document (the comment is invisible in previews; re-rendering reads the `.d2` files, so edit those). Converts the older `<details>` format. Renders everything to a temp dir first, so a failing diagram changes nothing.
36
+ - `render.py` (`kingmadoc render <doc>`): renders each ```` ```d2 ```` block with ELK to `img/<doc>-<n>.svg` (dark theme; D2 via `d2_binary.ensure_d2`) and, by default, `img/<doc>-<n>.png` via `raster.svg_to_png` (resvg; D2's embedded WOFF fonts unpacked to TTF and the `d2-N-font-*` CSS names mapped to family + weight, else text is missing), moves its source to `img/<doc>-<n>.d2`, and leaves only `<!-- kingmadoc:diagram img/<doc>-<n>.d2 -->` + the PNG (`--format svg`: the SVG) in the document; `diagram_warnings` = dark-mode styles (only reported for `--format svg`) + crowding (> 12 arrows, doubled pairs) (the comment is invisible in previews; re-rendering reads the `.d2` files, so edit those). Converts the older `<details>` format. Renders everything to a temp dir first, so a failing diagram changes nothing.
37
37
  - `d2_binary.py`: `ensure_d2` finds D2 (`KINGMADOC_D2`, PATH, cache) or downloads the pinned release once into the user cache; the SHA-256 per platform is pinned in `D2_ASSETS` (update version + all hashes together, cross-checked with the release's SHA256SUMS). `skills.py` + `kingmadoc skills install`: the skills in `skill/` ship as package data (hatch force-include) and are copied into the agent's skills dir; a `.kingmadoc-skill.json` manifest per skill folder records the installed hashes, so a reinstall updates unchanged files and only blocks local edits (`PREVIOUS_RELEASES` is the frozen hash list for installs from before the manifest).
38
38
  - `skill/explaining-code/`: second product skill; explains existing code (feature, branch, project, part) with D2 pictures, one folder per subject `docs/explain/<NNNN>-<slug>/README.md` (`explain.py` + `kingmadoc explain new`: next ID, never reused, same subject reuses its folder; regenerates the index `docs/explain/README.md`, also after `render` of such a file; `explain status` → `freshness`: `git diff --relative --name-only <Based on commit>` over the paths the explainer names in code spans (else the whole project), excluding `docs/explain`; `render` names a README's images `figure-<n>`). `SKILL.md` = workflow and D2 examples; the formats are `reference/arc42.md` (default) and `reference/c4.md`, chosen by `explain.format`; `reference/split.md` for `explain.documents: split` (cover + `functional.md` + `technical.md`); `reference/c4-model.md` (Simon Brown's C4: abstractions, 7 diagrams, notation, D2 style block, checklist) and `reference/models.md` (decision table + rules + one example per other model). `tests/test_skill_explaining_code.py` checks its format and compiles every D2 example with the real `d2` when available (`D2_BIN`); `tests/test_skill_models.py` checks every example obeys its model's rules. D2 pitfalls: quote labels with `[`/`:`; `source-arrowhead` only works on `<->`.
39
39
  - `facts/` (`kingmadoc explain facts`): pure parsers `projects.project_references` (.csproj) and `data_model.data_model` (EF Core via `DbSet<T>`, Prisma, Django, SQLAlchemy, TypeORM; regex, test files skipped) → frozen `Entity`/`Field`/`Relation`; `routes.routes` (ASP.NET controllers + minimal APIs, Next.js app/pages, Django urls + decorators, FastAPI, Flask, Express; `access` = what the code states), `services.services` (.NET DI), `js_modules.js_dependencies` (relative + tsconfig `paths` imports, `plan.dependencies.collapse` above 25 modules, `max_nodes=None` for the raw graph), `dominators.private_modules` (Cooper–Harvey–Kennedy on the raw Python + JS graphs; virtual root above the modules nothing imports; a single entry point is left out); `branch.branch_changes` (git I/O via `git.run_git`, merge base → working tree); `collect.collect_facts` reads files from the `CodebaseReport` and renders Markdown/JSON.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: kingmadoc
3
- Version: 0.3.0.dev8
3
+ Version: 0.3.0.dev9
4
4
  Summary: Feature docs for AI coding agents: a plan with C4 diagrams before the code, a verification doc after.
5
5
  Project-URL: Homepage, https://github.com/ATkingma/KingmaDoc
6
6
  Project-URL: Issues, https://github.com/ATkingma/KingmaDoc/issues
@@ -42,6 +42,7 @@ Requires-Python: >=3.11
42
42
  Requires-Dist: click>=8.1
43
43
  Requires-Dist: jinja2>=3.1
44
44
  Requires-Dist: pyyaml>=6.0
45
+ Requires-Dist: resvg-py<0.6,>=0.5
45
46
  Provides-Extra: dev
46
47
  Requires-Dist: hypothesis>=6.100; extra == 'dev'
47
48
  Requires-Dist: import-linter>=2.1; extra == 'dev'
@@ -107,12 +108,11 @@ them are updated; files you edited are kept (`--force` replaces them too).
107
108
 
108
109
  Nothing else to install: the first `kingmadoc render` downloads the D2 diagram renderer by
109
110
  itself (pinned version, checksum-verified; `KINGMADOC_D2_DOWNLOAD=0` turns that off).
110
- VS Code opens a `.md` file as text, which shows no pictures. `kingmadoc skills install`
111
- asks whether VS Code should open explainers (`docs/explain/`) as a rendered preview
112
- instead, so you see the pictures right away (one setting in `.vscode/settings.json`;
113
- `--vscode` / `--no-vscode` answer up front). Without it, press Ctrl+Shift+V in an
114
- explainer. Visual Studio shows a preview by default. The pictures follow VS Code's light
115
- or dark theme.
111
+ The pictures are PNG files linked from plain Markdown, so every Markdown preview shows
112
+ them: Visual Studio and Rider (preview on by default), VS Code (Ctrl+Shift+V), GitHub,
113
+ GitLab and Bitbucket. `render --format svg` links the SVG instead, which follows dark
114
+ mode. VS Code opens `.md` as text: `kingmadoc skills install` offers to make it open
115
+ explainers as a preview (`--vscode-user` for every folder, `--vscode` for this project).
116
116
 
117
117
  ### pip
118
118
 
@@ -158,8 +158,7 @@ agent adds the models the code calls for (UML sequence, state machine, class, ac
158
158
  with swimlanes, use case, ER, data flow with trust boundaries, context map), each drawn
159
159
  by its own notation rules. No stories, no audit. Install it next to the first one and ask the agent to "explain <feature / branch /
160
160
  project>". `kingmadoc skills install` installs it together with the first skill; the
161
- pictures are rendered with `kingmadoc render`; they follow the viewer's light or dark
162
- theme (`--light` for light only).
161
+ pictures are rendered with `kingmadoc render` as PNG, which every editor shows.
163
162
 
164
163
  - The Codex and Copilot files are loaded in **every** session (about 17 KB). Codex
165
164
  stops reading `AGENTS.md` files after 32 KiB in total by default
@@ -53,12 +53,11 @@ them are updated; files you edited are kept (`--force` replaces them too).
53
53
 
54
54
  Nothing else to install: the first `kingmadoc render` downloads the D2 diagram renderer by
55
55
  itself (pinned version, checksum-verified; `KINGMADOC_D2_DOWNLOAD=0` turns that off).
56
- VS Code opens a `.md` file as text, which shows no pictures. `kingmadoc skills install`
57
- asks whether VS Code should open explainers (`docs/explain/`) as a rendered preview
58
- instead, so you see the pictures right away (one setting in `.vscode/settings.json`;
59
- `--vscode` / `--no-vscode` answer up front). Without it, press Ctrl+Shift+V in an
60
- explainer. Visual Studio shows a preview by default. The pictures follow VS Code's light
61
- or dark theme.
56
+ The pictures are PNG files linked from plain Markdown, so every Markdown preview shows
57
+ them: Visual Studio and Rider (preview on by default), VS Code (Ctrl+Shift+V), GitHub,
58
+ GitLab and Bitbucket. `render --format svg` links the SVG instead, which follows dark
59
+ mode. VS Code opens `.md` as text: `kingmadoc skills install` offers to make it open
60
+ explainers as a preview (`--vscode-user` for every folder, `--vscode` for this project).
62
61
 
63
62
  ### pip
64
63
 
@@ -104,8 +103,7 @@ agent adds the models the code calls for (UML sequence, state machine, class, ac
104
103
  with swimlanes, use case, ER, data flow with trust boundaries, context map), each drawn
105
104
  by its own notation rules. No stories, no audit. Install it next to the first one and ask the agent to "explain <feature / branch /
106
105
  project>". `kingmadoc skills install` installs it together with the first skill; the
107
- pictures are rendered with `kingmadoc render`; they follow the viewer's light or dark
108
- theme (`--light` for light only).
106
+ pictures are rendered with `kingmadoc render` as PNG, which every editor shows.
109
107
 
110
108
  - The Codex and Copilot files are loaded in **every** session (about 17 KB). Codex
111
109
  stops reading `AGENTS.md` files after 32 KiB in total by default
@@ -26,6 +26,8 @@ dependencies = [
26
26
  "click>=8.1",
27
27
  "jinja2>=3.1",
28
28
  "pyyaml>=6.0",
29
+ # SVG -> PNG for rendered diagrams (offline, wheels for every platform).
30
+ "resvg-py>=0.5,<0.6",
29
31
  ]
30
32
 
31
33
  [project.urls]
@@ -88,6 +90,10 @@ select = ["E", "W", "F", "I", "UP", "B", "S"]
88
90
  strict = true
89
91
  files = ["src"]
90
92
 
93
+ [[tool.mypy.overrides]]
94
+ module = ["resvg_py"] # ships no type information
95
+ ignore_missing_imports = true
96
+
91
97
  [tool.pytest.ini_options]
92
98
  testpaths = ["tests"]
93
99
  addopts = "-ra"
@@ -125,7 +131,7 @@ source_modules = [
125
131
  "kingmadoc.plan", "kingmadoc.verify", "kingmadoc.facts", "kingmadoc.diagrams",
126
132
  "kingmadoc.about", "kingmadoc.adr", "kingmadoc.config", "kingmadoc.d2_binary",
127
133
  "kingmadoc.documents", "kingmadoc.exceptions", "kingmadoc.explain", "kingmadoc.git",
128
- "kingmadoc.naming", "kingmadoc.plandoc", "kingmadoc.render", "kingmadoc.skills",
134
+ "kingmadoc.naming", "kingmadoc.plandoc", "kingmadoc.raster", "kingmadoc.render", "kingmadoc.skills",
129
135
  "kingmadoc.templating", "kingmadoc.vscode",
130
136
  ]
131
137
  forbidden_modules = ["click", "kingmadoc.cli"]
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: explaining-code
3
3
  description: "Explains existing code with rendered diagrams (C4 in Simon Brown's notation; UML sequence, state, class, activity, use case; ER; data flow) and short tables, as an arc42 or compact C4 document, one file or split into functional and technical. Fixes code blindness, e.g. after an agent wrote the code. Scope: a feature, a branch or PR, a whole project, or a folder, service or module. Use when the user asks to explain, describe, document, map, diagram, draw, visualise or give an overview of existing code or architecture; asks how something works, what it does, how the parts fit together, where something happens, or what a branch, PR, commit or task changed; wants onboarding, a walkthrough, a codebase tour, an architecture or design document, arc42, C4, UML, sequence, ER or deployment diagrams of existing code; or no longer understands the code. The request may be in any language. Not for features that are not built yet."
4
- version: 5.7.0
4
+ version: 5.8.0
5
5
  allowed-tools: [Read, Write, Glob, Grep, Bash]
6
6
  ---
7
7
 
@@ -158,7 +158,7 @@ Write every diagram in **D2** (Step 5 turns them into images).
158
158
  eight figures in total.
159
159
  3. **Place them** where the format (or, when split, [reference/split.md](reference/split.md))
160
160
  says: e.g. arc42 section 6 for flows and lifecycles, section 8 for data and domain.
161
- 4. **Readable in dark mode:** the images follow the viewer's light or dark theme. Give
161
+ 4. **Readable in dark mode:** the SVG next to each PNG follows the viewer's theme. Give
162
162
  every shape you fill (`fill:`) a `font-color` too, and black dots a grey `stroke`.
163
163
  Never set a `font-color` without a fill (titles, labels: the theme picks the colour),
164
164
  and never fill white: boundaries and nodes are `fill: transparent`. Put
@@ -219,10 +219,12 @@ An explainer is not finished until its diagrams are pictures. Run:
219
219
  kingmadoc render docs/explain/<NNNN>-<slug>/*.md
220
220
  ```
221
221
 
222
- It replaces each diagram block with its image (`img/figure-<n>.svg` for `README.md`,
223
- `img/functional-<n>.svg` and `img/technical-<n>.svg` when split) and moves the D2
224
- source next to it (`.d2`), so the documents show only pictures. Each image has a light
225
- and a dark theme and follows the viewer's (`--light`: light only). To change a diagram
222
+ It replaces each diagram block with a PNG (`img/figure-<n>.png` for `README.md`,
223
+ `img/functional-<n>.png` and `img/technical-<n>.png` when split), which every editor
224
+ and Git host shows: VS Code, Visual Studio, Rider, GitHub, GitLab, Bitbucket. Next to it
225
+ go an `.svg` (sharp, follows dark mode) and the D2 source (`.d2`), so the documents show
226
+ only pictures. Never convert the images yourself (PNG scripts, HTML or PDF exports):
227
+ `render` already makes the PNG. To change a diagram
226
228
  later, edit its `.d2` file and render again. The first time it downloads D2 by itself
227
229
  (checksum-verified). **Never ask the user to install D2**, even when `d2` is not
228
230
  on the PATH: `kingmadoc render` does not need it.
@@ -232,7 +234,7 @@ on the PATH: `kingmadoc render` does not need it.
232
234
  `pipx upgrade kingmadoc` (installed from GitHub: `pipx reinstall kingmadoc`), then render.
233
235
  - `kingmadoc` is not installed at all: render with `d2` if it happens to be available
234
236
  (save each diagram as `img/figure-<n>.d2` in the subject's folder, run
235
- `d2 --pad 20 <that>.d2 <that>.svg`, and replace the block with
237
+ `d2 --pad 20 --layout elk <that>.d2 <that>.svg`, and replace the block with
236
238
  `![<caption>](img/figure-<n>.svg)`); otherwise say that installing KingmaDoc gives
237
239
  the pictures.
238
240
 
@@ -63,7 +63,7 @@ Text in `<angle brackets>` is filled in; leave out subsections marked optional.
63
63
  | **Scope** | <feature / branch `<branch>` vs `<base>` / project / part `<path>`> |
64
64
  | **Stack** | <languages, frameworks, data stores> |
65
65
  | **Entry points** | <`path`, …> |
66
- | **Based on** | <commit hash (branch)> · <ISO date> · KingmaDoc skill explaining-code 5.7.0 (arc42) |
66
+ | **Based on** | <commit hash (branch)> · <ISO date> · KingmaDoc skill explaining-code 5.8.0 (arc42) |
67
67
 
68
68
  ## What changed (branch only)
69
69
 
@@ -22,7 +22,7 @@ Text in `<angle brackets>` is filled in.
22
22
  | **Scope** | <feature / branch `<branch>` vs `<base>` / project / part `<path>`> |
23
23
  | **Stack** | <languages, frameworks, data stores> |
24
24
  | **Entry points** | <`path`, …> |
25
- | **Based on** | <commit hash (branch)> · <ISO date> · KingmaDoc skill explaining-code 5.7.0 |
25
+ | **Based on** | <commit hash (branch)> · <ISO date> · KingmaDoc skill explaining-code 5.8.0 |
26
26
 
27
27
  ## In short
28
28
 
@@ -43,7 +43,7 @@ in its first line. `kingmadoc render` takes all three files at once.
43
43
  | **Scope** | <feature / branch `<branch>` vs `<base>` / project / part `<path>`> |
44
44
  | **Stack** | <languages, frameworks, data stores> |
45
45
  | **Entry points** | <`path`, …> |
46
- | **Based on** | <commit hash (branch)> · <ISO date> · KingmaDoc skill explaining-code 5.7.0 (split) |
46
+ | **Based on** | <commit hash (branch)> · <ISO date> · KingmaDoc skill explaining-code 5.8.0 (split) |
47
47
 
48
48
  <at most three plain sentences: what it is, for whom, what it does>
49
49
 
@@ -49,7 +49,7 @@ from kingmadoc.plan.generator import (
49
49
  render_plan,
50
50
  )
51
51
  from kingmadoc.plandoc import check_plan, generated_at, parse_plan, set_status
52
- from kingmadoc.render import diagram_warnings, render_file
52
+ from kingmadoc.render import IMAGE_FORMATS, diagram_warnings, render_file
53
53
  from kingmadoc.skills import AGENT_DIRS, install_skills
54
54
  from kingmadoc.verify.changes import detect_changes
55
55
  from kingmadoc.verify.commands import MARKER_FILES, detect_commands, run_check
@@ -61,7 +61,11 @@ from kingmadoc.verify.deviations import (
61
61
  )
62
62
  from kingmadoc.verify.locate import find_plan, verify_output_path
63
63
  from kingmadoc.verify.report import render_verify, verify_status
64
- from kingmadoc.vscode import enable_markdown_preview, preview_enabled
64
+ from kingmadoc.vscode import (
65
+ enable_markdown_preview,
66
+ enable_user_markdown_preview,
67
+ preview_enabled,
68
+ )
65
69
 
66
70
  ROOT_OPTION = click.option(
67
71
  "--root",
@@ -438,18 +442,29 @@ def adr(title: str, root: Path, config_path: Path | None, status: str) -> None:
438
442
  "--light", is_flag=True, help="Light images only (by default they follow dark mode too)."
439
443
  )
440
444
  @click.option("--verbose", is_flag=True, help="Print every image path (default: one line).")
441
- def render_command(documents: tuple[Path, ...], light: bool, verbose: bool) -> None:
442
- """Render the D2 diagrams in DOCUMENTS to SVG images and embed them.
445
+ @click.option(
446
+ "--format",
447
+ "image_format",
448
+ type=click.Choice(IMAGE_FORMATS),
449
+ default="png",
450
+ show_default=True,
451
+ help="What the document links: png shows in every Markdown viewer; svg follows dark mode.",
452
+ )
453
+ def render_command(
454
+ documents: tuple[Path, ...], light: bool, verbose: bool, image_format: str
455
+ ) -> None:
456
+ """Render the D2 diagrams in DOCUMENTS to images and embed them.
443
457
 
444
- Images go to img/<document>-<n>.svg next to each document (img/figure-<n>.svg for a
445
- README.md), their D2 sources to img/*.d2. D2 is downloaded once (pinned,
446
- checksum-verified) unless it is on PATH or in KINGMADOC_D2. Prints the image paths.
458
+ Each diagram becomes img/<document>-<n>.png (linked: every Markdown viewer shows it)
459
+ and .svg (sharp, follows dark mode; --format svg links it instead), with its D2 source
460
+ in img/*.d2; a README.md's images are img/figure-<n>. D2 is downloaded once (pinned,
461
+ checksum-verified) unless it is on PATH or in KINGMADOC_D2.
447
462
  """
448
463
  try:
449
464
  d2 = ensure_d2(lambda message: click.echo(message, err=True))
450
465
  hinted = False
451
466
  for document in documents:
452
- images = render_file(document, d2, dark=not light)
467
+ images = render_file(document, d2, dark=not light, image_format=image_format)
453
468
  index = index_path(document)
454
469
  if index is not None:
455
470
  _write_explain_index(index.parent)
@@ -459,7 +474,7 @@ def render_command(documents: tuple[Path, ...], light: bool, verbose: bool) -> N
459
474
  click.echo(
460
475
  "VS Code shows the pictures in the preview: open the file and press "
461
476
  "Ctrl+Shift+V (macOS: Cmd+Shift+V), or run `kingmadoc skills install "
462
- "--vscode` to always open explainers as a preview.",
477
+ "--vscode-user` to always open explainers as a preview.",
463
478
  err=True,
464
479
  )
465
480
  if not images:
@@ -474,9 +489,10 @@ def render_command(documents: tuple[Path, ...], light: bool, verbose: bool) -> N
474
489
  noun = "image" if len(images) == 1 else "images"
475
490
  click.echo(f"{document}: {len(images)} {noun} ({span})")
476
491
  for image in images:
477
- source = image.with_suffix(".d2")
492
+ source = image.with_suffix(".d2") # next to the .png and .svg
478
493
  for warning in diagram_warnings(source.read_text(encoding="utf-8")):
479
- if light and warning.endswith("(dark mode)"):
494
+ dark_mode = not light and image_format == "svg"
495
+ if not dark_mode and warning.endswith("(dark mode)"):
480
496
  continue
481
497
  click.echo(f"{source}: {warning}", err=True)
482
498
  except KingmaDocError as exc:
@@ -626,10 +642,18 @@ def skills_group() -> None:
626
642
  @click.option(
627
643
  "--vscode/--no-vscode",
628
644
  default=None,
629
- help="Make VS Code open explainers as a rendered preview (.vscode/settings.json), or "
630
- "not. Without either, a terminal asks; other runs only print a tip.",
645
+ help="Make VS Code open this project's explainers as a rendered preview "
646
+ "(.vscode/settings.json; only when this folder is the open workspace), or not.",
631
647
  )
632
- def skills_install(root: Path, agent: str, force: bool, vscode: bool | None) -> None:
648
+ @click.option(
649
+ "--vscode-user",
650
+ is_flag=True,
651
+ help="Make VS Code open explainers as a rendered preview in every folder (user "
652
+ "settings). Without a VS Code option, a terminal asks; other runs print a tip.",
653
+ )
654
+ def skills_install(
655
+ root: Path, agent: str, force: bool, vscode: bool | None, vscode_user: bool
656
+ ) -> None:
633
657
  """Install the KingmaDoc skills into the project for AGENT (Agent Skills standard).
634
658
 
635
659
  Also offers to make VS Code open docs/explain/ as a rendered preview, so the pictures
@@ -637,16 +661,21 @@ def skills_install(root: Path, agent: str, force: bool, vscode: bool | None) ->
637
661
  """
638
662
  try:
639
663
  result = install_skills(root, agent, force)
640
- if vscode is None and not preview_enabled(root) and _interactive():
664
+ asked = vscode is None and not vscode_user
665
+ if asked and not preview_enabled(root) and _interactive():
641
666
  try:
642
- vscode = click.confirm(
667
+ vscode_user = click.confirm(
643
668
  "Make VS Code open explainers (docs/explain/) as a rendered preview, so "
644
- "the pictures show right away?", default=True, err=True,
669
+ "the pictures show right away? (VS Code user settings)",
670
+ default=True, err=True,
645
671
  )
646
672
  except click.Abort: # stdin closed without an answer: change nothing
647
673
  click.echo("", err=True)
648
- vscode = False
649
- preview = enable_markdown_preview(root) if vscode else None
674
+ preview = None
675
+ if vscode_user:
676
+ preview = enable_user_markdown_preview()
677
+ elif vscode:
678
+ preview = enable_markdown_preview(root)
650
679
  except KingmaDocError as exc:
651
680
  raise click.ClickException(str(exc)) from exc
652
681
  for path in result.written:
@@ -657,9 +686,10 @@ def skills_install(root: Path, agent: str, force: bool, vscode: bool | None) ->
657
686
  click.echo(f"{len(result.up_to_date)} skill file(s) already up to date.", err=True)
658
687
  if preview:
659
688
  click.echo(preview, err=True)
660
- elif vscode is None and not preview_enabled(root):
689
+ elif asked and not preview_enabled(root):
661
690
  click.echo(
662
- "Tip: --vscode makes VS Code open explainers (docs/explain/) as a rendered preview.",
691
+ "Tip: --vscode-user makes VS Code open explainers (docs/explain/) as a rendered "
692
+ "preview, with the pictures.",
663
693
  err=True,
664
694
  )
665
695
 
@@ -0,0 +1,141 @@
1
+ """SVG to PNG for rendered diagrams, so every Markdown viewer shows them.
2
+
3
+ VS Code, Visual Studio, Rider, GitHub, GitLab and Bitbucket all show a PNG in Markdown;
4
+ several block or mishandle SVG. The conversion runs offline with resvg (a small wheel,
5
+ no system libraries). D2 embeds its fonts (Source Sans Pro, as WOFF subsets) in each
6
+ SVG; resvg cannot read WOFF or CSS ``@font-face``, so the fonts are unpacked to TrueType
7
+ and the CSS names D2 made up (``d2-123-font-bold``) become the real family and weight.
8
+ Without that, text is measured with another font and clipped.
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ import base64
14
+ import re
15
+ import struct
16
+ import tempfile
17
+ import zlib
18
+ from collections.abc import Mapping
19
+ from pathlib import Path
20
+ from types import MappingProxyType
21
+
22
+ import resvg_py
23
+
24
+ from kingmadoc.exceptions import RenderError
25
+
26
+ # 2x: sharp on high-DPI screens, still small (a typical diagram is 100-300 KB).
27
+ ZOOM = 2
28
+ _FONT_FACE = re.compile(
29
+ r"@font-face\s*\{\s*font-family:\s*(d2-\d+-font-[\w-]+);\s*"
30
+ r"src:\s*url\(\"data:application/font-woff;base64,([A-Za-z0-9+/=]+)\"\)[^}]*\}"
31
+ )
32
+ _FONT_USE = re.compile(r"font-family:\s*\"?d2-\d+-font-([\w-]+)\"?")
33
+ _STYLES: Mapping[str, str] = MappingProxyType({
34
+ "regular": "font-weight: normal; font-style: normal",
35
+ "bold": "font-weight: bold; font-style: normal",
36
+ "semibold": "font-weight: 600; font-style: normal",
37
+ "italic": "font-weight: normal; font-style: italic",
38
+ })
39
+
40
+
41
+ def svg_to_png(svg: str) -> bytes:
42
+ """Rasterize a D2 SVG with its own fonts.
43
+
44
+ Args:
45
+ svg: The SVG text D2 wrote.
46
+
47
+ Returns:
48
+ PNG bytes at :data:`ZOOM` times the SVG size (light theme: resvg ignores the
49
+ dark-mode media query, so the picture reads on any background).
50
+
51
+ Raises:
52
+ RenderError: If the SVG cannot be rasterized.
53
+ """
54
+ faces = _FONT_FACE.findall(svg)
55
+ families: dict[str, str] = {}
56
+ with tempfile.TemporaryDirectory(prefix="kingmadoc-fonts-") as tmp:
57
+ files = []
58
+ for index, (name, data) in enumerate(faces):
59
+ try:
60
+ sfnt = woff_to_sfnt(base64.b64decode(data))
61
+ families[name] = font_family(sfnt) or "sans-serif"
62
+ except (RenderError, ValueError, struct.error, zlib.error):
63
+ continue # an unreadable font: its text falls back to another font
64
+ path = Path(tmp) / f"{index}.ttf"
65
+ path.write_bytes(sfnt)
66
+ files.append(str(path))
67
+ family = next(iter(families.values()), "sans-serif")
68
+ text = _FONT_USE.sub(
69
+ lambda m: f'font-family: "{family}"; {_STYLES.get(m.group(1), _STYLES["regular"])}',
70
+ re.sub(r"@font-face\s*\{[^}]*\}", "", svg),
71
+ )
72
+ try:
73
+ png = resvg_py.svg_to_bytes(
74
+ svg_string=text, font_files=files, skip_system_fonts=bool(files), zoom=ZOOM
75
+ )
76
+ except Exception as exc: # noqa: BLE001 - resvg raises bare exceptions (and panics)
77
+ raise RenderError(f"Could not convert the diagram to PNG: {exc}") from exc
78
+ return bytes(png)
79
+
80
+
81
+ def woff_to_sfnt(data: bytes) -> bytes:
82
+ """Unpack a WOFF 1.0 font to the TrueType/OpenType file it wraps.
83
+
84
+ Args:
85
+ data: The WOFF file.
86
+
87
+ Returns:
88
+ The sfnt (``.ttf``/``.otf``) bytes.
89
+
90
+ Raises:
91
+ RenderError: If ``data`` is not WOFF 1.0.
92
+ """
93
+ if len(data) < 44 or data[:4] != b"wOFF":
94
+ raise RenderError("not a WOFF 1.0 font")
95
+ flavor, _length, count = struct.unpack(">4xIIH", data[:14])
96
+ tables = []
97
+ for i in range(count):
98
+ tag, offset, size, original, checksum = struct.unpack(
99
+ ">4sIIII", data[44 + 20 * i : 64 + 20 * i]
100
+ )
101
+ raw = data[offset : offset + size]
102
+ tables.append((tag, zlib.decompress(raw) if size < original else raw, checksum))
103
+ tables.sort()
104
+ search = 1
105
+ while search * 2 <= count:
106
+ search *= 2
107
+ header = struct.pack(
108
+ ">IHHHH", flavor, count, search * 16, search.bit_length() - 1, count * 16 - search * 16
109
+ )
110
+ offset = 12 + 16 * count
111
+ directory, body = b"", b""
112
+ for tag, table, checksum in tables:
113
+ directory += struct.pack(">4sIII", tag, checksum, offset + len(body), len(table))
114
+ body += table + b"\0" * (-len(table) % 4)
115
+ return header + directory + body
116
+
117
+
118
+ def font_family(sfnt: bytes) -> str:
119
+ """Return a font's family name (name table, name ID 1), or "" when it has none.
120
+
121
+ Args:
122
+ sfnt: A TrueType/OpenType font.
123
+
124
+ Returns:
125
+ E.g. ``"Source Sans Pro"``.
126
+ """
127
+ count = struct.unpack(">H", sfnt[4:6])[0]
128
+ for i in range(count):
129
+ tag, _checksum, offset, length = struct.unpack(">4sIII", sfnt[12 + 16 * i : 28 + 16 * i])
130
+ if tag != b"name":
131
+ continue
132
+ table = sfnt[offset : offset + length]
133
+ _format, records, strings = struct.unpack(">HHH", table[:6])
134
+ for j in range(records):
135
+ platform, _enc, _lang, name_id, size, start = struct.unpack(
136
+ ">HHHHHH", table[6 + 12 * j : 18 + 12 * j]
137
+ )
138
+ if name_id == 1:
139
+ raw = table[strings + start : strings + start + size]
140
+ return raw.decode("utf-16-be" if platform in (0, 3) else "latin-1", "replace")
141
+ return ""
@@ -27,12 +27,15 @@ from pathlib import Path
27
27
  from kingmadoc.documents import write_document
28
28
  from kingmadoc.exceptions import RenderError
29
29
  from kingmadoc.explain import EXPLAINER_FILE, index_path
30
+ from kingmadoc.raster import svg_to_png
30
31
 
31
32
  # Seconds per diagram; D2's own default is 120, but diagrams in docs are small.
32
33
  D2_TIMEOUT = 60
33
34
  # Pixels around each image; D2's default of 100 wastes space in a document.
34
35
  D2_PAD = 20
35
36
  IMAGE_MODE = 0o644
37
+ # What `kingmadoc render` links in the document (PNG shows in every Markdown viewer).
38
+ IMAGE_FORMATS: tuple[str, ...] = ("png", "svg")
36
39
  # D2's own dark theme; the SVG switches to it when the viewer uses dark mode.
37
40
  D2_DARK_THEME = 200
38
41
  # Straight, right-angled arrows (see _layout_args); bundled with D2 like dagre.
@@ -57,8 +60,15 @@ _LEGACY = re.compile(
57
60
  _HEADING = re.compile(r"^#{1,6} +(.+?) *$", re.M)
58
61
 
59
62
 
60
- def render_file(path: Path, d2: Sequence[str], dark: bool = True) -> list[Path]:
61
- """Render every D2 diagram in a Markdown file to an SVG and embed only the images.
63
+ def render_file(
64
+ path: Path, d2: Sequence[str], dark: bool = True, image_format: str = "png"
65
+ ) -> list[Path]:
66
+ """Render every D2 diagram in a Markdown file to images and embed only the images.
67
+
68
+ Each diagram becomes ``img/<name>.svg`` (with a dark theme) and, for
69
+ ``image_format="png"``, ``img/<name>.png`` too; the document links the chosen format.
70
+ PNG shows in every Markdown viewer (VS Code, Visual Studio, Rider, GitHub, GitLab,
71
+ Bitbucket); several block or mishandle SVG.
62
72
 
63
73
  All diagrams are rendered before anything is written: if one fails, neither the
64
74
  document nor any file in ``img/`` changes.
@@ -66,7 +76,8 @@ def render_file(path: Path, d2: Sequence[str], dark: bool = True) -> list[Path]:
66
76
  Args:
67
77
  path: The Markdown document.
68
78
  d2: Command that runs D2 (see :func:`kingmadoc.d2_binary.ensure_d2`).
69
- dark: Also embed a dark theme, used when the viewer is in dark mode.
79
+ dark: Also embed a dark theme in the SVG, used when the viewer is in dark mode.
80
+ image_format: What the document links: one of :data:`IMAGE_FORMATS`.
70
81
 
71
82
  Returns:
72
83
  The written image paths, in document order (empty if there are no diagrams).
@@ -80,6 +91,8 @@ def render_file(path: Path, d2: Sequence[str], dark: bool = True) -> list[Path]:
80
91
  if not items:
81
92
  return []
82
93
 
94
+ if image_format not in IMAGE_FORMATS:
95
+ raise RenderError(f"Unknown image format {image_format!r}; use {', '.join(IMAGE_FORMATS)}")
83
96
  image_dir = path.parent / "img"
84
97
  sources = [_source(item, path) for item in items]
85
98
  stem = _image_stem(path)
@@ -91,20 +104,27 @@ def render_file(path: Path, d2: Sequence[str], dark: bool = True) -> list[Path]:
91
104
  _render(source, Path(tmp), n, path, [*d2, *_theme_args(dark), *_layout_args(source)])
92
105
  for n, source in enumerate(sources, start=1)
93
106
  ]
107
+ if image_format == "png":
108
+ for svg in rendered:
109
+ svg.with_suffix(".png").write_bytes(svg_to_png(svg.read_text(encoding="utf-8")))
94
110
  image_dir.mkdir(parents=True, exist_ok=True)
95
111
  for name, source, svg in zip(names, sources, rendered, strict=True):
96
112
  write_document(image_dir / f"{name}.d2", source.rstrip("\n") + "\n", overwrite=True)
97
- os.replace(svg, image_dir / f"{name}.svg")
98
- # d2 writes its output private (0600); images are for everyone who reads docs.
99
- (image_dir / f"{name}.svg").chmod(IMAGE_MODE)
113
+ for suffix in (".svg", ".png") if image_format == "png" else (".svg",):
114
+ target = image_dir / f"{name}{suffix}"
115
+ os.replace(svg.with_suffix(suffix), target)
116
+ # d2 writes its output private (0600); images are for everyone.
117
+ target.chmod(IMAGE_MODE)
118
+ if image_format == "svg":
119
+ (image_dir / f"{name}.png").unlink(missing_ok=True)
100
120
  _remove_stale_files(image_dir, stem, set(names))
101
121
  _remove_renamed_files(image_dir, path, items, set(names))
102
122
 
103
123
  for item, name in zip(reversed(items), reversed(names), strict=True):
104
124
  alt = _nearest_heading(text, item.start()) or "Diagram"
105
- text = text[: item.start()] + _embed(alt, name) + text[item.end() :]
125
+ text = text[: item.start()] + _embed(alt, name, image_format) + text[item.end() :]
106
126
  write_document(path, text, overwrite=True)
107
- return [image_dir / f"{name}.svg" for name in names]
127
+ return [image_dir / f"{name}.{image_format}" for name in names]
108
128
 
109
129
 
110
130
  def diagram_warnings(source: str) -> list[str]:
@@ -264,9 +284,9 @@ def _theme_args(dark: bool) -> list[str]:
264
284
  return ["--dark-theme", str(D2_DARK_THEME)] if dark else []
265
285
 
266
286
 
267
- def _embed(alt: str, name: str) -> str:
287
+ def _embed(alt: str, name: str, image_format: str) -> str:
268
288
  alt = alt.replace("[", "(").replace("]", ")")
269
- return f"<!-- kingmadoc:diagram img/{name}.d2 -->\n![{alt}](img/{name}.svg)"
289
+ return f"<!-- kingmadoc:diagram img/{name}.d2 -->\n![{alt}](img/{name}.{image_format})"
270
290
 
271
291
 
272
292
  def _nearest_heading(text: str, position: int) -> str | None:
@@ -298,13 +318,13 @@ def _remove_renamed_files(
298
318
  name = Path(ref).stem if ref else ""
299
319
  if name in keep or not own.fullmatch(name):
300
320
  continue
301
- for suffix in (".svg", ".d2"):
321
+ for suffix in (".svg", ".png", ".d2"):
302
322
  (image_dir / f"{name}{suffix}").unlink(missing_ok=True)
303
323
 
304
324
 
305
325
  def _remove_stale_files(image_dir: Path, stem: str, keep: set[str]) -> None:
306
326
  """Delete images and sources of this document's diagrams that no longer exist."""
307
- pattern = re.compile(rf"({re.escape(stem)}-\d+)\.(svg|d2)")
327
+ pattern = re.compile(rf"({re.escape(stem)}-\d+)\.(svg|png|d2)")
308
328
  for file in image_dir.iterdir():
309
329
  match = pattern.fullmatch(file.name)
310
330
  if match and match.group(1) not in keep: