claude-dev-env 2.9.0 → 2.10.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 (289) hide show
  1. package/CLAUDE.md +1 -1
  2. package/_shared/pr-loop/CLAUDE.md +1 -1
  3. package/_shared/pr-loop/audit-contract.md +17 -6
  4. package/_shared/pr-loop/audit-reply-template.md +4 -4
  5. package/_shared/pr-loop/code-rules-gate.md +3 -5
  6. package/_shared/pr-loop/fix-protocol.md +2 -3
  7. package/_shared/pr-loop/gh-payloads.md +1 -1
  8. package/_shared/pr-loop/scripts/CLAUDE.md +1 -1
  9. package/_shared/pr-loop/scripts/README.md +1 -1
  10. package/_shared/pr-loop/scripts/code_rules_gate.py +2 -0
  11. package/_shared/pr-loop/scripts/code_rules_gate_parts/gate_running.py +16 -1
  12. package/_shared/pr-loop/scripts/code_rules_gate_parts/git_blob_readers.py +11 -5
  13. package/_shared/pr-loop/scripts/preflight.py +9 -4
  14. package/_shared/pr-loop/scripts/reviews_disabled.py +50 -22
  15. package/_shared/pr-loop/scripts/tests/conftest.py +20 -0
  16. package/_shared/pr-loop/scripts/tests/test_claude_permissions_common.py +6 -6
  17. package/_shared/pr-loop/scripts/tests/test_reviews_disabled.py +50 -6
  18. package/_shared/pr-loop/scripts/tests/test_revoke_project_claude_permissions.py +1 -1
  19. package/_shared/pr-loop/state-schema.md +5 -14
  20. package/agents/CLAUDE.md +2 -2
  21. package/agents/clean-coder.md +58 -548
  22. package/agents/code-quality-agent.md +10 -2
  23. package/agents/code-verifier.md +1 -1
  24. package/agents/test_agent_frontmatter.py +32 -40
  25. package/audit-rubrics/CLAUDE.md +2 -1
  26. package/audit-rubrics/audit-categories.json +704 -0
  27. package/audit-rubrics/prompts/category-i-concurrency.md +1 -1
  28. package/bin/CLAUDE.md +16 -5
  29. package/bin/ever-shipped-skills.mjs +2 -0
  30. package/bin/install-plan.mjs +402 -0
  31. package/bin/install-transaction.mjs +455 -0
  32. package/bin/install.mjs +593 -147
  33. package/bin/install.plan.test.mjs +194 -0
  34. package/bin/install.profile-root.test.mjs +154 -0
  35. package/bin/install.profiles.test.mjs +253 -0
  36. package/bin/install.settings-defaults.test.mjs +200 -0
  37. package/bin/install.transaction.test.mjs +400 -0
  38. package/bin/install.uninstall-transaction.test.mjs +418 -0
  39. package/bin/merge_managed_permissions.mjs +130 -0
  40. package/bin/resolve-install-root.mjs +181 -0
  41. package/bin/select-install-targets.mjs +401 -0
  42. package/commands/CLAUDE.md +0 -2
  43. package/docs/references/CLAUDE.md +1 -0
  44. package/docs/references/advisor-tool.md +1 -1
  45. package/docs/references/prose-style-enforcement.md +25 -0
  46. package/hooks/blocking/CLAUDE.md +6 -6
  47. package/hooks/blocking/_path_setup.py +9 -5
  48. package/hooks/blocking/code_rules_docstrings.py +124 -30
  49. package/hooks/blocking/code_rules_enforcer.py +161 -16
  50. package/hooks/blocking/code_rules_shared.py +40 -23
  51. package/hooks/blocking/config/CLAUDE.md +3 -5
  52. package/hooks/blocking/config/prose_style_enforcement_constants.py +38 -0
  53. package/hooks/blocking/config/test_prose_style_enforcement_constants.py +45 -0
  54. package/hooks/blocking/eli11_reply_enforcer.py +70 -113
  55. package/hooks/blocking/hedging_language_blocker.py +103 -20
  56. package/hooks/blocking/hook_prose_detector_consistency.py +6 -0
  57. package/hooks/blocking/intent_only_ending_blocker.py +6 -0
  58. package/hooks/blocking/plain_language_blocker.py +139 -20
  59. package/hooks/blocking/pre_tool_use_dispatcher.py +102 -20
  60. package/hooks/blocking/state_description_blocker.py +7 -1
  61. package/hooks/blocking/tdd_enforcer.py +8 -0
  62. package/hooks/blocking/test__path_setup.py +28 -0
  63. package/hooks/blocking/test_code_rules_enforcer_agent_home_tooling.py +99 -0
  64. package/hooks/blocking/test_code_rules_enforcer_docstring_args_span_scope.py +232 -10
  65. package/hooks/blocking/test_code_rules_enforcer_ephemeral.py +1 -1
  66. package/hooks/blocking/test_code_rules_enforcer_join_separator_magic.py +41 -0
  67. package/hooks/blocking/test_code_rules_enforcer_string_magic.py +98 -0
  68. package/hooks/blocking/test_eli11_reply_enforcer.py +98 -165
  69. package/hooks/blocking/test_hedging_language_blocker.py +120 -1
  70. package/hooks/blocking/test_hook_prose_detector_consistency.py +28 -8
  71. package/hooks/blocking/test_intent_only_ending_blocker.py +27 -2
  72. package/hooks/blocking/test_package_inventory_stale_blocker.py +11 -4
  73. package/hooks/blocking/test_plain_language_blocker.py +129 -19
  74. package/hooks/blocking/test_plain_language_blocker_allowlist.py +70 -26
  75. package/hooks/blocking/test_pre_tool_use_dispatcher.py +99 -26
  76. package/hooks/blocking/test_pre_tool_use_dispatcher_native.py +87 -50
  77. package/hooks/blocking/test_state_description_blocker.py +45 -2
  78. package/hooks/blocking/test_stop_dispatcher.py +11 -7
  79. package/hooks/blocking/test_volatile_path_in_post_blocker.py +12 -12
  80. package/hooks/blocking/volatile_path_in_post_blocker.py +2 -2
  81. package/hooks/hooks.json +15 -0
  82. package/hooks/hooks_constants/CLAUDE.md +14 -3
  83. package/hooks/hooks_constants/ask_user_question_shape.py +281 -0
  84. package/hooks/hooks_constants/code_rules_enforcer_constants.py +2 -1
  85. package/hooks/hooks_constants/eli11_reply_enforcer_constants.py +5 -12
  86. package/hooks/hooks_constants/hedging_uncertainty_constants.py +42 -0
  87. package/hooks/hooks_constants/issue_tracker_session_starter_constants.py +23 -0
  88. package/hooks/hooks_constants/orchestrator_auto_starter_constants.py +23 -0
  89. package/hooks/hooks_constants/piped_pytest_blocker_constants.py +4 -1
  90. package/hooks/hooks_constants/plain_language_blocker_constants.py +4 -1
  91. package/hooks/hooks_constants/pre_tool_use_dispatcher_constants.py +6 -0
  92. package/hooks/hooks_constants/project_paths_reader.py +31 -4
  93. package/hooks/hooks_constants/prose_matcher_precision_constants.py +40 -0
  94. package/hooks/hooks_constants/pytest_invocation.py +354 -0
  95. package/hooks/hooks_constants/session_start_injector.py +163 -0
  96. package/hooks/hooks_constants/session_start_injector_constants.py +46 -0
  97. package/hooks/hooks_constants/shell_command_pipeline.py +397 -0
  98. package/hooks/hooks_constants/shell_command_segments.py +5 -0
  99. package/hooks/hooks_constants/test_ask_user_question_shape.py +167 -0
  100. package/hooks/hooks_constants/test_project_paths_reader.py +29 -0
  101. package/hooks/hooks_constants/test_prose_metrics_parity.py +8 -0
  102. package/hooks/hooks_constants/test_pytest_invocation.py +130 -0
  103. package/hooks/hooks_constants/test_session_start_injector.py +168 -0
  104. package/hooks/hooks_constants/test_shell_command_pipeline.py +135 -0
  105. package/hooks/hooks_constants/volatile_path_in_post_blocker_constants.py +1 -1
  106. package/hooks/hooks_constants/working_style_prompt_constants.py +30 -0
  107. package/hooks/observability/CLAUDE.md +2 -0
  108. package/hooks/observability/prose_matcher_advisory.py +237 -0
  109. package/hooks/observability/test_prose_matcher_advisory.py +143 -0
  110. package/hooks/session/CLAUDE.md +9 -1
  111. package/hooks/session/_path_setup.py +13 -0
  112. package/hooks/session/issue_tracker_session_starter.py +135 -0
  113. package/hooks/session/orchestrator_auto_starter.py +100 -0
  114. package/hooks/session/test__path_setup.py +28 -0
  115. package/hooks/session/test_issue_tracker_session_starter.py +104 -0
  116. package/hooks/session/test_orchestrator_auto_starter.py +99 -0
  117. package/hooks/session/test_working_style_prompt.py +47 -0
  118. package/hooks/session/untracked_repo_detector.py +1 -24
  119. package/hooks/session/working_style_prompt.py +36 -0
  120. package/hooks/validators/_path_setup.py +19 -0
  121. package/hooks/validators/run_all_validators.py +8 -13
  122. package/installable-surfaces.manifest.json +21 -0
  123. package/output-styles/CLAUDE.md +1 -3
  124. package/package.json +4 -2
  125. package/rules/CLAUDE.md +1 -0
  126. package/rules/durable-post-artifacts.md +2 -2
  127. package/rules/eli11-replies.md +6 -1
  128. package/rules/hedging-claims.md +4 -2
  129. package/rules/long-horizon-autonomy.md +3 -1
  130. package/rules/opus5-communication-contract.md +45 -0
  131. package/rules/plain-language.md +2 -2
  132. package/rules/research-mode.md +1 -1
  133. package/scripts/CLAUDE.md +11 -0
  134. package/scripts/Sync-RepoMain.ps1 +215 -0
  135. package/scripts/active_capability_references.py +218 -0
  136. package/scripts/ci/windows-installer-lifecycle.ps1 +78 -0
  137. package/scripts/claude_chain_runner.py +394 -6
  138. package/scripts/claude_chain_usage.py +1 -1
  139. package/scripts/codex_compat_materializer.py +105 -85
  140. package/scripts/dev_env_scripts_constants/CLAUDE.md +2 -0
  141. package/scripts/dev_env_scripts_constants/active_capability_constants.py +46 -0
  142. package/scripts/dev_env_scripts_constants/claude_chain_constants.py +74 -0
  143. package/scripts/dev_env_scripts_constants/verify_installable_package_constants.py +116 -0
  144. package/scripts/profile-isolation-launchers/config/mcp-bundles.json +25 -0
  145. package/scripts/profile-isolation-launchers/config/profile-isolation-constants.mjs +60 -0
  146. package/scripts/profile-isolation-launchers/config/profiles.manifest.json +54 -0
  147. package/scripts/profile-isolation-launchers/config/shared-allowlist.json +64 -0
  148. package/scripts/profile-isolation-launchers/launcher-runtime.mjs +180 -0
  149. package/scripts/profile-isolation-launchers/lib/profile-manifest.mjs +288 -0
  150. package/scripts/profile-isolation-launchers/mcp-bundles.mjs +275 -0
  151. package/scripts/profile-isolation-launchers/profile-isolation-contract.test.mjs +221 -0
  152. package/scripts/profile-isolation-launchers/tests/launcher-runtime.test.mjs +108 -0
  153. package/scripts/profile-isolation-launchers/tests/mcp-bundles.test.mjs +147 -0
  154. package/scripts/profile-isolation-launchers/tests/shortcut-contract.test.ps1 +102 -0
  155. package/scripts/profile-isolation-launchers/tests/version-compatibility.test.mjs +210 -0
  156. package/scripts/profile-isolation-launchers/version-compatibility.mjs +299 -0
  157. package/scripts/profile-isolation-launchers/windows/shortcut-inventory.ps1 +127 -0
  158. package/scripts/profile-isolation-launchers/windows/shortcut-manifest.json +51 -0
  159. package/scripts/profile-isolation-launchers/windows/shortcut-reconcile.ps1 +77 -0
  160. package/scripts/spawn_grok_batch.py +3 -0
  161. package/scripts/test_active_capability_references.py +108 -0
  162. package/scripts/test_claude_chain_runner.py +414 -82
  163. package/scripts/test_claude_chain_usage.py +12 -12
  164. package/scripts/test_resolve_worker_spawn.py +2 -2
  165. package/scripts/test_verify_installable_package.py +208 -0
  166. package/scripts/tests/test_codex_compat_materializer.py +33 -0
  167. package/scripts/verify_installable_package.py +612 -0
  168. package/settings.json +10 -0
  169. package/skills/CLAUDE.md +2 -0
  170. package/skills/_shared/pr-loop/scripts/CLAUDE.md +1 -0
  171. package/skills/_shared/pr-loop/scripts/audit_category_schema.py +355 -0
  172. package/skills/_shared/pr-loop/scripts/skills_pr_loop_constants/CLAUDE.md +1 -0
  173. package/skills/_shared/pr-loop/scripts/skills_pr_loop_constants/audit_category_schema_constants.py +32 -0
  174. package/skills/_shared/pr-loop/scripts/skills_pr_loop_constants/path_resolver_constants.py +7 -19
  175. package/skills/_shared/pr-loop/scripts/test_audit_category_schema.py +94 -0
  176. package/skills/_shared/pr-loop/scripts/test_build_audit_prompt.py +21 -0
  177. package/skills/autoconverge/reference/convergence.md +2 -1
  178. package/skills/autoconverge/reference/stop-conditions.md +5 -3
  179. package/skills/beat-sheet/SKILL.md +54 -0
  180. package/skills/beat-sheet/reference/visual-beats.md +29 -0
  181. package/skills/bugteam/CONSTRAINTS.md +4 -4
  182. package/skills/bugteam/EXAMPLES.md +1 -1
  183. package/skills/bugteam/reference/README.md +1 -1
  184. package/skills/e-code-review/SKILL.md +26 -5
  185. package/skills/e-code-review/reference/effort-evaluation.md +35 -0
  186. package/skills/e-code-review/reference/medium.md +15 -4
  187. package/skills/e-code-review/scripts/config/e_code_review_effort_constants/__init__.py +41 -0
  188. package/skills/e-code-review/scripts/config/e_code_review_effort_constants/effort_constants.py +40 -0
  189. package/skills/e-code-review/scripts/e_code_review_scripts_constants/finding_pipeline_constants.py +49 -0
  190. package/skills/e-code-review/scripts/effort_defaults_evidence.json +186 -0
  191. package/skills/e-code-review/scripts/effort_evaluation.py +362 -0
  192. package/skills/e-code-review/scripts/finding_pipeline.py +140 -0
  193. package/skills/e-code-review/scripts/fixtures/demanding.json +26 -0
  194. package/skills/e-code-review/scripts/fixtures/easy.json +14 -0
  195. package/skills/e-code-review/scripts/fixtures/medium.json +20 -0
  196. package/skills/e-code-review/scripts/grok_code_review.py +16 -7
  197. package/skills/e-code-review/scripts/test_effort_evaluation.py +180 -0
  198. package/skills/e-code-review/scripts/test_finding_pipeline.py +197 -0
  199. package/skills/e-code-review/scripts/test_grok_code_review.py +77 -0
  200. package/skills/plan-to-pr/scripts/create_packet.py +4 -4
  201. package/skills/plan-to-pr/scripts/load_skill_constants.py +41 -0
  202. package/skills/plan-to-pr/scripts/validate_packet.py +4 -4
  203. package/skills/plan-to-pr/scripts/validate_protocol.py +4 -1
  204. package/skills/plan-to-pr/scripts/validate_run.py +4 -1
  205. package/skills/pr-converge/scripts/check_convergence.py +21 -19
  206. package/skills/pr-converge/scripts/check_convergence_availability.py +50 -7
  207. package/skills/pr-converge/scripts/conftest.py +35 -0
  208. package/skills/pr-converge/scripts/test_check_convergence_availability.py +65 -0
  209. package/skills/pr-converge/scripts/test_check_convergence_codex.py +11 -1
  210. package/skills/pr-converge/scripts/test_check_convergence_contract.py +9 -2
  211. package/skills/pr-loop-cloud-transport/SKILL.md +1 -1
  212. package/skills/rebase/SKILL.md +15 -3
  213. package/skills/reviewer-gates/SKILL.md +2 -2
  214. package/skills/show/SKILL.md +51 -0
  215. package/skills/show/references/accessibility.md +7 -0
  216. package/skills/show/references/art.md +3 -0
  217. package/skills/show/references/charts.md +3 -0
  218. package/skills/show/references/core-design.md +14 -0
  219. package/skills/show/references/erds.md +3 -0
  220. package/skills/show/references/flowcharts.md +3 -0
  221. package/skills/show/references/host-and-html.md +3 -0
  222. package/skills/show/references/illustrative-diagrams.md +10 -0
  223. package/skills/show/references/interaction.md +3 -0
  224. package/skills/show/references/mockups.md +3 -0
  225. package/skills/show/references/quality-gates.md +7 -0
  226. package/skills/show/references/structural-diagrams.md +3 -0
  227. package/skills/show/references/subject-inventory.md +21 -0
  228. package/skills/show/references/svg-contract.md +22 -0
  229. package/skills/show/routing.yaml +30 -0
  230. package/skills/show/samples/pr1262-v2.svg +222 -0
  231. package/skills/show/scripts/README.md +6 -0
  232. package/skills/show/scripts/validate-artifact.py +91 -0
  233. package/skills/show/scripts/validate-package.py +18 -0
  234. package/skills/show/templates/html-widget.html +4 -0
  235. package/skills/show/templates/svg-base.svg +19 -0
  236. package/skills/show/tests/fixtures/css-var.svg +6 -0
  237. package/skills/show/tests/fixtures/dead-ref.svg +7 -0
  238. package/skills/show/tests/fixtures/filled-glyph.svg +8 -0
  239. package/skills/show/tests/fixtures/inherited-fill.svg +18 -0
  240. package/skills/show/tests/fixtures/invalid.svg +1 -0
  241. package/skills/show/tests/fixtures/large-canvas.svg +21 -0
  242. package/skills/show/tests/fixtures/unfilled-connector.svg +15 -0
  243. package/skills/show/tests/fixtures/valid.html +1 -0
  244. package/skills/show/tests/test_validate-artifact.py +74 -0
  245. package/skills/show/tests/test_validators.py +59 -0
  246. package/skills/show/workflows/create-visual.md +13 -0
  247. package/skills/show/workflows/review-visual.md +20 -0
  248. package/skills/split-pr/SKILL.md +85 -0
  249. package/skills/split-pr/reference/path-layers.md +16 -0
  250. package/skills/split-pr/reference/proposal-format.md +15 -0
  251. package/skills/split-pr/reference/split-further-loop.md +10 -0
  252. package/skills/split-pr/reference/splitting-principles.md +26 -0
  253. package/skills/split-pr/scripts/analyze_pr.py +279 -0
  254. package/skills/split-pr/scripts/categorize_files.py +106 -0
  255. package/skills/split-pr/scripts/config/__init__.py +1 -0
  256. package/skills/split-pr/scripts/config/dependency_constants.py +14 -0
  257. package/skills/split-pr/scripts/config/git_operations_constants.py +36 -0
  258. package/skills/split-pr/scripts/config/packing_constants.py +61 -0
  259. package/skills/split-pr/scripts/config/plan_constants.py +49 -0
  260. package/skills/split-pr/scripts/config/split_pr_constants.py +110 -0
  261. package/skills/split-pr/scripts/execute_split_slices.py +82 -0
  262. package/skills/split-pr/scripts/pack_files_into_slices.py +212 -0
  263. package/skills/split-pr/scripts/split_pr_dependency_graph.py +70 -0
  264. package/skills/split-pr/scripts/split_pr_git_operations.py +184 -0
  265. package/skills/split-pr/scripts/split_pr_layer_order.py +58 -0
  266. package/skills/split-pr/scripts/split_pr_paginate.py +119 -0
  267. package/skills/split-pr/scripts/split_pr_process_runner.py +52 -0
  268. package/skills/split-pr/scripts/split_pr_script_types.py +126 -0
  269. package/skills/split-pr/scripts/split_pr_title.py +41 -0
  270. package/skills/split-pr/scripts/test_analyze_pr.py +228 -0
  271. package/skills/split-pr/scripts/test_categorize_files.py +55 -0
  272. package/skills/split-pr/scripts/test_categorize_files_packing.py +59 -0
  273. package/skills/split-pr/scripts/test_execute_split_slices.py +99 -0
  274. package/skills/split-pr/scripts/test_split_pr_dependency_graph.py +47 -0
  275. package/skills/split-pr/scripts/test_split_pr_git_operations.py +125 -0
  276. package/skills/split-pr/scripts/test_split_pr_layer_order.py +36 -0
  277. package/skills/split-pr/scripts/test_split_pr_paginate.py +65 -0
  278. package/skills/split-pr/scripts/test_split_pr_script_types.py +73 -0
  279. package/skills/split-pr/scripts/test_split_pr_title.py +28 -0
  280. package/skills/split-pr/scripts/test_verify_dependency_graph.py +46 -0
  281. package/skills/split-pr/scripts/test_verify_plan.py +56 -0
  282. package/skills/split-pr/scripts/test_verify_plan_contract.py +50 -0
  283. package/skills/split-pr/scripts/test_verify_plan_path_normalization.py +45 -0
  284. package/skills/split-pr/scripts/verify_dependency_graph.py +111 -0
  285. package/skills/split-pr/scripts/verify_plan.py +139 -0
  286. package/system-prompts/software-engineer.xml +11 -2
  287. package/commands/initialize.md +0 -90
  288. package/commands/stubcheck.md +0 -88
  289. package/output-styles/caveman-agent.md +0 -37
@@ -0,0 +1,222 @@
1
+ <svg viewBox="0 0 1340 1680" xmlns="http://www.w3.org/2000/svg" width="1340" height="1680" role="img">
2
+ <title>PR 1262: unreachable checkpoint-skip branches removed</title>
3
+ <desc>Three panels. One: submit() stamps not_started, a value absent from workflow_order, so every should_skip_checkpoint reading is False and all five skip outlets are dead ends. Two: the four valves, the review bypass, and the LIVE_WALLPAPER spur are deleted; the checkpoint stamps remain on the straight pipe. Three: real resume is a product-level sieve in the orchestrator that turns THEME, WALLPAPER into WALLPAPER.</desc>
4
+ <defs>
5
+ <marker id="aRed" markerUnits="userSpaceOnUse" markerWidth="16" markerHeight="14" refX="15" refY="7" orient="auto">
6
+ <path d="M0,0 L16,7 L0,14 Z" fill="#C0392B"/>
7
+ </marker>
8
+ <marker id="aGrn" markerUnits="userSpaceOnUse" markerWidth="16" markerHeight="14" refX="15" refY="7" orient="auto">
9
+ <path d="M0,0 L16,7 L0,14 Z" fill="#1E8449"/>
10
+ </marker>
11
+ <marker id="aAmb" markerUnits="userSpaceOnUse" markerWidth="16" markerHeight="14" refX="15" refY="7" orient="auto">
12
+ <path d="M0,0 L16,7 L0,14 Z" fill="#B9770E"/>
13
+ </marker>
14
+ <marker id="aGry" markerUnits="userSpaceOnUse" markerWidth="14" markerHeight="12" refX="13" refY="6" orient="auto">
15
+ <path d="M0,0 L14,6 L0,12 Z" fill="#7A7A7A"/>
16
+ </marker>
17
+ </defs>
18
+
19
+ <rect x="0" y="0" width="1340" height="1680" fill="#FAF7EF"/>
20
+
21
+ <!-- ================= PANEL 1 ================= -->
22
+ <text x="60" y="56" font-family="Helvetica,Arial,sans-serif" font-size="30" font-weight="bold" fill="#C0392B">1 · TODAY — every gate reads a blank stamp</text>
23
+
24
+ <rect x="58" y="94" width="286" height="230" rx="12" fill="#EDEAE1" stroke="#3A3A3A" stroke-width="3"/>
25
+ <text x="201" y="128" text-anchor="middle" font-family="Helvetica,Arial,sans-serif" font-size="23" font-weight="bold" fill="#1A1A1A">submit()</text>
26
+ <rect x="175" y="144" width="52" height="32" fill="#8A8A8A" stroke="#3A3A3A" stroke-width="3"/>
27
+ <rect x="191" y="176" width="20" height="24" fill="#8A8A8A" stroke="#3A3A3A" stroke-width="3"/>
28
+ <rect x="147" y="200" width="108" height="26" fill="#C0392B" stroke="#7B241C" stroke-width="3"/>
29
+ <rect x="96" y="248" width="212" height="52" rx="6" fill="#FFFFFF" stroke="#C0392B" stroke-width="3"/>
30
+ <text x="202" y="281" text-anchor="middle" font-family="Menlo,Consolas,monospace" font-size="22" fill="#C0392B">not_started</text>
31
+ <text x="201" y="346" text-anchor="middle" font-family="Menlo,Consolas,monospace" font-size="17" fill="#6E6E6E">submission_service.py:173</text>
32
+
33
+ <line x1="348" y1="274" x2="404" y2="274" stroke="#C0392B" stroke-width="6" marker-end="url(#aRed)"/>
34
+
35
+ <rect x="416" y="94" width="308" height="268" rx="12" fill="#FFFFFF" stroke="#3A3A3A" stroke-width="3"/>
36
+ <text x="570" y="124" text-anchor="middle" font-family="Menlo,Consolas,monospace" font-size="21" font-weight="bold" fill="#1A1A1A">workflow_order()</text>
37
+ <rect x="438" y="136" width="264" height="32" rx="5" fill="#F2EFE6" stroke="#9A9A9A" stroke-width="2"/>
38
+ <text x="570" y="159" text-anchor="middle" font-family="Menlo,Consolas,monospace" font-size="20" fill="#3A3A3A">binary_apks_uploaded</text>
39
+ <rect x="438" y="172" width="264" height="32" rx="5" fill="#F2EFE6" stroke="#9A9A9A" stroke-width="2"/>
40
+ <text x="570" y="195" text-anchor="middle" font-family="Menlo,Consolas,monospace" font-size="20" fill="#3A3A3A">app_info_saved</text>
41
+ <rect x="438" y="208" width="264" height="32" rx="5" fill="#F2EFE6" stroke="#9A9A9A" stroke-width="2"/>
42
+ <text x="570" y="231" text-anchor="middle" font-family="Menlo,Consolas,monospace" font-size="20" fill="#3A3A3A">country_price_saved</text>
43
+ <rect x="438" y="244" width="264" height="32" rx="5" fill="#F2EFE6" stroke="#9A9A9A" stroke-width="2"/>
44
+ <text x="570" y="267" text-anchor="middle" font-family="Menlo,Consolas,monospace" font-size="20" fill="#3A3A3A">publication_saved</text>
45
+ <rect x="438" y="280" width="264" height="32" rx="5" fill="#F2EFE6" stroke="#9A9A9A" stroke-width="2"/>
46
+ <text x="570" y="303" text-anchor="middle" font-family="Menlo,Consolas,monospace" font-size="20" fill="#3A3A3A">review_saved</text>
47
+ <rect x="438" y="316" width="264" height="32" rx="5" fill="#F2EFE6" stroke="#9A9A9A" stroke-width="2"/>
48
+ <text x="570" y="339" text-anchor="middle" font-family="Menlo,Consolas,monospace" font-size="20" fill="#3A3A3A">submitted_for_review</text>
49
+
50
+ <rect x="740" y="130" width="152" height="40" rx="6" fill="#FFFFFF" stroke="#C0392B" stroke-width="3" stroke-dasharray="7 5"/>
51
+ <text x="816" y="158" text-anchor="middle" font-family="Menlo,Consolas,monospace" font-size="19" fill="#C0392B">not_started</text>
52
+ <rect x="740" y="214" width="152" height="40" rx="6" fill="#FFFFFF" stroke="#C0392B" stroke-width="3" stroke-dasharray="7 5" opacity="0.5"/>
53
+ <text x="816" y="242" text-anchor="middle" font-family="Menlo,Consolas,monospace" font-size="19" fill="#C0392B" opacity="0.5">not_started</text>
54
+ <rect x="740" y="298" width="152" height="40" rx="6" fill="#FFFFFF" stroke="#C0392B" stroke-width="3" stroke-dasharray="7 5" opacity="0.26"/>
55
+ <text x="816" y="326" text-anchor="middle" font-family="Menlo,Consolas,monospace" font-size="19" fill="#C0392B" opacity="0.26">not_started</text>
56
+ <line x1="816" y1="174" x2="816" y2="356" stroke="#C0392B" stroke-width="5" stroke-dasharray="10 8" marker-end="url(#aRed)"/>
57
+ <text x="816" y="394" text-anchor="middle" font-family="Menlo,Consolas,monospace" font-size="28" font-weight="bold" fill="#C0392B">False</text>
58
+
59
+ <text x="944" y="196" font-family="Menlo,Consolas,monospace" font-size="20" fill="#C0392B">no slot matches</text>
60
+ <text x="944" y="222" font-family="Menlo,Consolas,monospace" font-size="20" fill="#C0392B">not_started</text>
61
+ <text x="944" y="254" font-family="Menlo,Consolas,monospace" font-size="17" fill="#6E6E6E">checkpoint.py · workflow_order</text>
62
+ <text x="944" y="276" font-family="Menlo,Consolas,monospace" font-size="17" fill="#6E6E6E">excludes NOT_STARTED</text>
63
+ <text x="944" y="326" font-family="Menlo,Consolas,monospace" font-size="20" fill="#1A1A1A">should_skip_checkpoint</text>
64
+ <text x="944" y="350" font-family="Menlo,Consolas,monospace" font-size="17" fill="#6E6E6E">submission_checkpoints.py:47</text>
65
+
66
+ <line x1="816" y1="402" x2="816" y2="426" stroke="#C0392B" stroke-width="5"/>
67
+ <line x1="210" y1="426" x2="1170" y2="426" stroke="#C0392B" stroke-width="5"/>
68
+
69
+ <rect x="86" y="480" width="1204" height="56" fill="#DFF0E4" stroke="#1E8449" stroke-width="4"/>
70
+ <line x1="86" y1="508" x2="1312" y2="508" stroke="#1E8449" stroke-width="8" marker-end="url(#aGrn)"/>
71
+
72
+ <g>
73
+ <line x1="210" y1="426" x2="210" y2="460" stroke="#C0392B" stroke-width="5" marker-end="url(#aRed)"/>
74
+ <rect x="166" y="462" width="88" height="92" rx="8" fill="#FFFFFF" stroke="#C0392B" stroke-width="3"/>
75
+ <circle cx="210" cy="508" r="16" fill="#DFF0E4" stroke="#1E8449" stroke-width="4"/>
76
+ <line x1="210" y1="554" x2="210" y2="584" stroke="#C0392B" stroke-width="5" stroke-dasharray="8 6"/>
77
+ <rect x="176" y="584" width="68" height="30" fill="#F6DDD9" stroke="#C0392B" stroke-width="3"/>
78
+ <line x1="188" y1="590" x2="232" y2="608" stroke="#C0392B" stroke-width="4"/>
79
+ <line x1="232" y1="590" x2="188" y2="608" stroke="#C0392B" stroke-width="4"/>
80
+ <text x="210" y="648" text-anchor="middle" font-family="Menlo,Consolas,monospace" font-size="18" fill="#1A1A1A">binary_apks_uploaded</text>
81
+ <text x="210" y="672" text-anchor="middle" font-family="Menlo,Consolas,monospace" font-size="16" fill="#6E6E6E">pipeline.py:114</text>
82
+ </g>
83
+ <g>
84
+ <line x1="450" y1="426" x2="450" y2="460" stroke="#C0392B" stroke-width="5" marker-end="url(#aRed)"/>
85
+ <rect x="406" y="462" width="88" height="92" rx="8" fill="#FFFFFF" stroke="#C0392B" stroke-width="3"/>
86
+ <circle cx="450" cy="508" r="16" fill="#DFF0E4" stroke="#1E8449" stroke-width="4"/>
87
+ <line x1="450" y1="554" x2="450" y2="584" stroke="#C0392B" stroke-width="5" stroke-dasharray="8 6"/>
88
+ <rect x="416" y="584" width="68" height="30" fill="#F6DDD9" stroke="#C0392B" stroke-width="3"/>
89
+ <line x1="428" y1="590" x2="472" y2="608" stroke="#C0392B" stroke-width="4"/>
90
+ <line x1="472" y1="590" x2="428" y2="608" stroke="#C0392B" stroke-width="4"/>
91
+ <text x="450" y="648" text-anchor="middle" font-family="Menlo,Consolas,monospace" font-size="18" fill="#1A1A1A">app_info_saved</text>
92
+ <text x="450" y="672" text-anchor="middle" font-family="Menlo,Consolas,monospace" font-size="16" fill="#6E6E6E">pipeline.py:174</text>
93
+ </g>
94
+ <g>
95
+ <line x1="690" y1="426" x2="690" y2="460" stroke="#C0392B" stroke-width="5" marker-end="url(#aRed)"/>
96
+ <rect x="646" y="462" width="88" height="92" rx="8" fill="#FFFFFF" stroke="#C0392B" stroke-width="3"/>
97
+ <circle cx="690" cy="508" r="16" fill="#DFF0E4" stroke="#1E8449" stroke-width="4"/>
98
+ <line x1="690" y1="554" x2="690" y2="584" stroke="#C0392B" stroke-width="5" stroke-dasharray="8 6"/>
99
+ <rect x="656" y="584" width="68" height="30" fill="#F6DDD9" stroke="#C0392B" stroke-width="3"/>
100
+ <line x1="668" y1="590" x2="712" y2="608" stroke="#C0392B" stroke-width="4"/>
101
+ <line x1="712" y1="590" x2="668" y2="608" stroke="#C0392B" stroke-width="4"/>
102
+ <text x="690" y="648" text-anchor="middle" font-family="Menlo,Consolas,monospace" font-size="18" fill="#1A1A1A">country_price_saved</text>
103
+ <text x="690" y="672" text-anchor="middle" font-family="Menlo,Consolas,monospace" font-size="16" fill="#6E6E6E">pipeline.py:208</text>
104
+ </g>
105
+ <g>
106
+ <line x1="930" y1="426" x2="930" y2="460" stroke="#C0392B" stroke-width="5" marker-end="url(#aRed)"/>
107
+ <rect x="886" y="462" width="88" height="92" rx="8" fill="#FFFFFF" stroke="#C0392B" stroke-width="3"/>
108
+ <circle cx="930" cy="508" r="16" fill="#DFF0E4" stroke="#1E8449" stroke-width="4"/>
109
+ <line x1="930" y1="554" x2="930" y2="584" stroke="#C0392B" stroke-width="5" stroke-dasharray="8 6"/>
110
+ <rect x="896" y="584" width="68" height="30" fill="#F6DDD9" stroke="#C0392B" stroke-width="3"/>
111
+ <line x1="908" y1="590" x2="952" y2="608" stroke="#C0392B" stroke-width="4"/>
112
+ <line x1="952" y1="590" x2="908" y2="608" stroke="#C0392B" stroke-width="4"/>
113
+ <text x="930" y="648" text-anchor="middle" font-family="Menlo,Consolas,monospace" font-size="18" fill="#1A1A1A">publication_saved</text>
114
+ <text x="930" y="672" text-anchor="middle" font-family="Menlo,Consolas,monospace" font-size="16" fill="#6E6E6E">pipeline.py:242</text>
115
+ </g>
116
+ <g>
117
+ <line x1="1170" y1="426" x2="1170" y2="460" stroke="#C0392B" stroke-width="5" marker-end="url(#aRed)"/>
118
+ <rect x="1126" y="462" width="88" height="92" rx="8" fill="#FFFFFF" stroke="#C0392B" stroke-width="3" stroke-dasharray="8 6"/>
119
+ <circle cx="1170" cy="508" r="16" fill="#DFF0E4" stroke="#1E8449" stroke-width="4"/>
120
+ <path d="M 1170 554 L 1170 588 L 1272 588 L 1272 540" fill="none" stroke="#C0392B" stroke-width="5" stroke-dasharray="8 6"/>
121
+ <line x1="1248" y1="562" x2="1280" y2="582" stroke="#C0392B" stroke-width="4"/>
122
+ <line x1="1280" y1="562" x2="1248" y2="582" stroke="#C0392B" stroke-width="4"/>
123
+ <text x="1170" y="648" text-anchor="middle" font-family="Menlo,Consolas,monospace" font-size="18" fill="#1A1A1A">review_saved</text>
124
+ <text x="1170" y="672" text-anchor="middle" font-family="Menlo,Consolas,monospace" font-size="16" fill="#6E6E6E">pipeline.py:314 · early return</text>
125
+ </g>
126
+
127
+ <line x1="60" y1="712" x2="1280" y2="712" stroke="#D5D0C2" stroke-width="3"/>
128
+
129
+ <!-- ================= PANEL 2 ================= -->
130
+ <text x="60" y="774" font-family="Helvetica,Arial,sans-serif" font-size="30" font-weight="bold" fill="#B9770E">2 · THE CUT — the gates come off, the stamps stay</text>
131
+
132
+ <path d="M 92 820 L 566 820 L 530 1058 L 128 1058 Z" fill="#FDF3E0" stroke="#B9770E" stroke-width="4"/>
133
+ <text x="329" y="856" text-anchor="middle" font-family="Menlo,Consolas,monospace" font-size="22" font-weight="bold" fill="#B9770E">should_skip_checkpoint</text>
134
+ <text x="329" y="882" text-anchor="middle" font-family="Menlo,Consolas,monospace" font-size="19" fill="#B9770E">−993 lines</text>
135
+
136
+ <rect x="126" y="900" width="182" height="42" rx="8" fill="#FFFFFF" stroke="#B9770E" stroke-width="3"/>
137
+ <text x="217" y="928" text-anchor="middle" font-family="Menlo,Consolas,monospace" font-size="18" fill="#B9770E">valve :114</text>
138
+ <rect x="326" y="900" width="182" height="42" rx="8" fill="#FFFFFF" stroke="#B9770E" stroke-width="3"/>
139
+ <text x="417" y="928" text-anchor="middle" font-family="Menlo,Consolas,monospace" font-size="18" fill="#B9770E">valve :174</text>
140
+ <rect x="138" y="954" width="182" height="42" rx="8" fill="#FFFFFF" stroke="#B9770E" stroke-width="3"/>
141
+ <text x="229" y="982" text-anchor="middle" font-family="Menlo,Consolas,monospace" font-size="18" fill="#B9770E">valve :208</text>
142
+ <rect x="338" y="954" width="182" height="42" rx="8" fill="#FFFFFF" stroke="#B9770E" stroke-width="3"/>
143
+ <text x="429" y="982" text-anchor="middle" font-family="Menlo,Consolas,monospace" font-size="18" fill="#B9770E">valve :242</text>
144
+ <rect x="150" y="1008" width="182" height="42" rx="8" fill="#FFFFFF" stroke="#B9770E" stroke-width="3" stroke-dasharray="7 5"/>
145
+ <text x="241" y="1036" text-anchor="middle" font-family="Menlo,Consolas,monospace" font-size="18" fill="#B9770E">bypass :314</text>
146
+ <rect x="350" y="1008" width="182" height="42" rx="8" fill="#FFFFFF" stroke="#B9770E" stroke-width="3" stroke-dasharray="7 5"/>
147
+ <text x="441" y="1036" text-anchor="middle" font-family="Menlo,Consolas,monospace" font-size="18" fill="#B9770E">spur :124</text>
148
+
149
+ <rect x="92" y="1084" width="474" height="98" rx="10" fill="#FFFFFF" stroke="#B9770E" stroke-width="3"/>
150
+ <text x="114" y="1118" font-family="Menlo,Consolas,monospace" font-size="20" fill="#1A1A1A">elif LIVE_WALLPAPER:</text>
151
+ <text x="140" y="1146" font-family="Menlo,Consolas,monospace" font-size="20" fill="#1A1A1A">capture_content_id()</text>
152
+ <text x="114" y="1172" font-family="Menlo,Consolas,monospace" font-size="17" fill="#6E6E6E">its only inlet was the valve above it</text>
153
+ <line x1="478" y1="1112" x2="518" y2="1140" stroke="#C0392B" stroke-width="5"/>
154
+ <line x1="518" y1="1112" x2="478" y2="1140" stroke="#C0392B" stroke-width="5"/>
155
+
156
+ <path d="M 706 946 L 624 900" fill="none" stroke="#B9770E" stroke-width="6" marker-end="url(#aAmb)"/>
157
+
158
+ <rect x="732" y="820" width="68" height="332" fill="#DFF0E4" stroke="#1E8449" stroke-width="4"/>
159
+ <line x1="766" y1="820" x2="766" y2="1188" stroke="#1E8449" stroke-width="8" marker-end="url(#aGrn)"/>
160
+
161
+ <g>
162
+ <rect x="814" y="828" width="40" height="40" rx="6" fill="#1E8449"/>
163
+ <text x="834" y="857" text-anchor="middle" font-family="Helvetica,Arial,sans-serif" font-size="24" font-weight="bold" fill="#FFFFFF">✓</text>
164
+ <text x="870" y="844" font-family="Menlo,Consolas,monospace" font-size="20" fill="#1A1A1A">binary_apks_uploaded</text>
165
+ <text x="870" y="866" font-family="Menlo,Consolas,monospace" font-size="20" fill="#1A1A1A">binary_devices_configured</text>
166
+ </g>
167
+ <g>
168
+ <rect x="814" y="894" width="40" height="40" rx="6" fill="#1E8449"/>
169
+ <text x="834" y="923" text-anchor="middle" font-family="Helvetica,Arial,sans-serif" font-size="24" font-weight="bold" fill="#FFFFFF">✓</text>
170
+ <text x="870" y="922" font-family="Menlo,Consolas,monospace" font-size="20" fill="#1A1A1A">app_info_saved</text>
171
+ </g>
172
+ <g>
173
+ <rect x="814" y="960" width="40" height="40" rx="6" fill="#1E8449"/>
174
+ <text x="834" y="989" text-anchor="middle" font-family="Helvetica,Arial,sans-serif" font-size="24" font-weight="bold" fill="#FFFFFF">✓</text>
175
+ <text x="870" y="988" font-family="Menlo,Consolas,monospace" font-size="20" fill="#1A1A1A">country_price_saved</text>
176
+ </g>
177
+ <g>
178
+ <rect x="814" y="1026" width="40" height="40" rx="6" fill="#1E8449"/>
179
+ <text x="834" y="1055" text-anchor="middle" font-family="Helvetica,Arial,sans-serif" font-size="24" font-weight="bold" fill="#FFFFFF">✓</text>
180
+ <text x="870" y="1054" font-family="Menlo,Consolas,monospace" font-size="20" fill="#1A1A1A">publication_saved</text>
181
+ </g>
182
+ <g>
183
+ <rect x="814" y="1092" width="40" height="40" rx="6" fill="#FFFFFF" stroke="#1E8449" stroke-width="3"/>
184
+ <text x="834" y="1121" text-anchor="middle" font-family="Helvetica,Arial,sans-serif" font-size="24" font-weight="bold" fill="#1E8449">✓</text>
185
+ <text x="870" y="1120" font-family="Menlo,Consolas,monospace" font-size="20" fill="#1A1A1A">review_saved</text>
186
+ <text x="870" y="1144" font-family="Menlo,Consolas,monospace" font-size="17" fill="#6E6E6E">withheld when the save is unverified</text>
187
+ </g>
188
+
189
+ <line x1="60" y1="1226" x2="1280" y2="1226" stroke="#D5D0C2" stroke-width="3"/>
190
+
191
+ <!-- ================= PANEL 3 ================= -->
192
+ <text x="60" y="1288" font-family="Helvetica,Arial,sans-serif" font-size="30" font-weight="bold" fill="#1E8449">3 · RESUME LIVES ONE FLOOR UP — a sieve, not a valve</text>
193
+
194
+ <rect x="80" y="1338" width="300" height="118" rx="10" fill="#FFFFFF" stroke="#1E8449" stroke-width="3"/>
195
+ <text x="230" y="1370" text-anchor="middle" font-family="Menlo,Consolas,monospace" font-size="19" fill="#6E6E6E">detected order</text>
196
+ <rect x="104" y="1386" width="118" height="46" rx="6" fill="#EDEAE1" stroke="#3A3A3A" stroke-width="2"/>
197
+ <text x="163" y="1417" text-anchor="middle" font-family="Menlo,Consolas,monospace" font-size="20" fill="#1A1A1A">THEME</text>
198
+ <rect x="234" y="1386" width="126" height="46" rx="6" fill="#EDEAE1" stroke="#3A3A3A" stroke-width="2"/>
199
+ <text x="297" y="1417" text-anchor="middle" font-family="Menlo,Consolas,monospace" font-size="20" fill="#1A1A1A">WALLPAPER</text>
200
+
201
+ <line x1="388" y1="1396" x2="452" y2="1396" stroke="#1E8449" stroke-width="6" marker-end="url(#aGrn)"/>
202
+
203
+ <path d="M 464 1328 L 792 1328 L 690 1458 L 566 1458 Z" fill="#DFF0E4" stroke="#1E8449" stroke-width="4"/>
204
+ <text x="628" y="1364" text-anchor="middle" font-family="Menlo,Consolas,monospace" font-size="21" font-weight="bold" fill="#1A1A1A">is_product_submitted</text>
205
+ <text x="628" y="1392" text-anchor="middle" font-family="Menlo,Consolas,monospace" font-size="17" fill="#1E8449">_orchestrator_theme_runner.py:253</text>
206
+ <line x1="528" y1="1418" x2="726" y2="1418" stroke="#1E8449" stroke-width="3" stroke-dasharray="6 8"/>
207
+
208
+ <path d="M 566 1462 L 566 1538 L 476 1538" fill="none" stroke="#7A7A7A" stroke-width="5" marker-end="url(#aGry)"/>
209
+ <rect x="304" y="1514" width="150" height="48" rx="6" fill="#EDEAE1" stroke="#7A7A7A" stroke-width="3"/>
210
+ <text x="379" y="1546" text-anchor="middle" font-family="Menlo,Consolas,monospace" font-size="20" fill="#7A7A7A">THEME</text>
211
+ <text x="304" y="1598" font-family="Menlo,Consolas,monospace" font-size="18" fill="#7A7A7A">already submitted per tracker</text>
212
+
213
+ <line x1="798" y1="1388" x2="860" y2="1388" stroke="#1E8449" stroke-width="6" marker-end="url(#aGrn)"/>
214
+ <rect x="874" y="1338" width="300" height="118" rx="10" fill="#FFFFFF" stroke="#1E8449" stroke-width="3"/>
215
+ <text x="1024" y="1370" text-anchor="middle" font-family="Menlo,Consolas,monospace" font-size="19" fill="#6E6E6E">order sent to submit()</text>
216
+ <rect x="960" y="1386" width="126" height="46" rx="6" fill="#DFF0E4" stroke="#1E8449" stroke-width="3"/>
217
+ <text x="1023" y="1417" text-anchor="middle" font-family="Menlo,Consolas,monospace" font-size="20" fill="#1E8449">WALLPAPER</text>
218
+
219
+ <text x="874" y="1518" font-family="Menlo,Consolas,monospace" font-size="22" fill="#1A1A1A">[THEME, WALLPAPER] → [WALLPAPER]</text>
220
+ <text x="874" y="1548" font-family="Menlo,Consolas,monospace" font-size="18" fill="#6E6E6E">product granularity · one submit() per product</text>
221
+ <text x="874" y="1598" font-family="Menlo,Consolas,monospace" font-size="18" fill="#6E6E6E">PR #1262 · base 5b40e42</text>
222
+ </svg>
@@ -0,0 +1,6 @@
1
+ # Show scripts
2
+
3
+ Run these scripts from any directory by passing an absolute artifact path. They use only the Python standard library.
4
+
5
+ - `validate-package.py` checks the package skeleton and hub links.
6
+ - `validate-artifact.py` checks SVG or HTML output contracts.
@@ -0,0 +1,91 @@
1
+ from pathlib import Path
2
+ import re
3
+ import sys
4
+
5
+ standard_canvas_width = 680.0
6
+ large_canvas_minimum_width = 1360.0
7
+ large_canvas_maximum_width = 2000.0
8
+ text_floor_at_standard_width = 16.0
9
+ connector_tags = {"line", "path", "polyline"}
10
+ tag_pattern = re.compile(r"<(/?)([a-zA-Z][\w:.-]*)([^>]*?)(/?)>", re.DOTALL)
11
+ fill_pattern = re.compile(r'\bfill=["\']([^"\']*)["\']')
12
+ style_fill_pattern = re.compile(r'style=["\'][^"\']*\bfill\s*:\s*([^;"\']+)')
13
+ marker_pattern = re.compile(r"\bmarker-(?:start|mid|end)=")
14
+ comment_pattern = re.compile(r"<!--.*?-->", re.DOTALL)
15
+
16
+
17
+ def find_unfilled_connectors(markup: str) -> list[str]:
18
+ """Name every connector whose fill resolves to something other than none.
19
+
20
+ A connector is a line, path, or polyline carrying a marker. Its fill comes
21
+ from its own attribute when it has one, and from the nearest enclosing
22
+ element otherwise::
23
+
24
+ <g fill="none"><line marker-end="url(#a)"/></g> ok: inherits none
25
+ <line fill="red" marker-end="url(#a)"/> flag: own fill wins
26
+ <path d="M0 0L9 9Z" fill="currentColor"/> ok: glyph, no marker
27
+
28
+ Args:
29
+ markup: The full SVG or HTML source text.
30
+
31
+ Returns:
32
+ The tag name of each offending connector, in document order.
33
+ """
34
+ inherited_fills = [""]
35
+ offenders = []
36
+ for each_tag in tag_pattern.finditer(markup):
37
+ is_closing, tag_name, attributes, is_self_closing = each_tag.groups()
38
+ if is_closing:
39
+ inherited_fills = inherited_fills[:-1] or [""]
40
+ continue
41
+ own_fill = style_fill_pattern.search(attributes) or fill_pattern.search(attributes)
42
+ effective_fill = own_fill.group(1).strip() if own_fill else inherited_fills[-1]
43
+ is_connector = tag_name in connector_tags and bool(marker_pattern.search(attributes))
44
+ if is_connector and effective_fill != "none": offenders.append(tag_name)
45
+ if not is_self_closing: inherited_fills.append(effective_fill)
46
+ return offenders
47
+
48
+
49
+ if len(sys.argv[1:]) != 1:
50
+ raise SystemExit("usage: validate-artifact.py <svg-or-html>")
51
+ path = Path(sys.argv[1])
52
+ if not path.is_file():
53
+ raise SystemExit(f"missing artifact: {path}")
54
+ text = comment_pattern.sub("", path.read_text(encoding="utf-8"))
55
+ errors = []
56
+ warnings = []
57
+ defined_properties = set(re.findall(r'(--[\w-]+)\s*:', text))
58
+ for each_property_name in re.findall(r'var\(\s*(--[\w-]+)', text):
59
+ if each_property_name not in defined_properties: errors.append(f"undefined custom property {each_property_name}")
60
+ defined_ids = set(re.findall(r'\bid=["\']([^"\']+)["\']', text))
61
+ referenced_ids = re.findall(r'href=["\']#([^"\']+)["\']', text) + re.findall(r'url\(\s*#([^)\s"\']+)', text)
62
+ for each_referenced_id in referenced_ids:
63
+ if each_referenced_id not in defined_ids: errors.append(f"reference to missing id #{each_referenced_id}")
64
+ if path.suffix.lower() == ".svg":
65
+ if not re.search(r'<svg[^>]+role=["\']img["\']', text): errors.append("SVG needs role=img")
66
+ viewbox = re.search(r'<svg[^>]+viewBox=["\']0 0 ([0-9.]+) ', text)
67
+ canvas_width = float(viewbox.group(1)) if viewbox else 0.0
68
+ is_standard_canvas = canvas_width == standard_canvas_width
69
+ is_large_canvas = large_canvas_minimum_width <= canvas_width <= large_canvas_maximum_width
70
+ if not is_standard_canvas and not is_large_canvas: errors.append("SVG viewBox width must be 680 or 1360-2000")
71
+ if not re.search(r'<svg[^>]*>\s*<title>.*?</title>\s*<desc>', text, re.DOTALL): errors.append("SVG title and desc must be first")
72
+ unfilled_connectors = find_unfilled_connectors(text)
73
+ if unfilled_connectors: errors.append(f"connectors need fill=none: {', '.join(sorted(set(unfilled_connectors)))}")
74
+ floor_width = max(canvas_width, standard_canvas_width)
75
+ text_floor = round(text_floor_at_standard_width * floor_width / standard_canvas_width)
76
+ sizes = [float(x) for x in re.findall(r'font-size=["\']([0-9.]+)', text)]
77
+ if any(size < text_floor for size in sizes): errors.append(f"text below {text_floor}px")
78
+ for each_marker in re.findall(r'<marker\b[^>]*>', text):
79
+ if not re.search(r'markerUnits=["\']userSpaceOnUse["\']', each_marker): warnings.append("marker without markerUnits=userSpaceOnUse scales with stroke width")
80
+ elif path.suffix.lower() in {".html", ".htm"}:
81
+ if not re.match(r'\s*<h2[^>]+class=["\']sr-only["\']', text): errors.append("HTML needs a first sr-only h2")
82
+ if re.search(r'position\s*:\s*fixed|overflow\s*:\s*(?:auto|scroll)', text): errors.append("fixed positioning and nested scrolling are forbidden")
83
+ else:
84
+ errors.append("artifact must be SVG or HTML")
85
+ for each_warning in warnings:
86
+ print(f"ARTIFACT WARNING: {each_warning}")
87
+ if errors:
88
+ print("ARTIFACT INVALID")
89
+ print("\n".join(errors))
90
+ sys.exit(1)
91
+ print(f"ARTIFACT VALID: {path.name}")
@@ -0,0 +1,18 @@
1
+ import re
2
+ from pathlib import Path
3
+ import sys
4
+
5
+ skill_root = Path(__file__).resolve().parents[1]
6
+ routing_text = (skill_root / "routing.yaml").read_text(encoding="utf-8")
7
+ routed = re.findall(r"(?:references|workflows|templates)/[\w.-]+", routing_text)
8
+ required = ["SKILL.md", "routing.yaml", "workflows/review-visual.md", *sorted(set(routed))]
9
+ text = (skill_root / "SKILL.md").read_text(encoding="utf-8")
10
+ errors = [p for p in required if not (skill_root / p).is_file()]
11
+ for each_token in ["routing.yaml", "workflows/create-visual.md"]:
12
+ if each_token not in text:
13
+ errors.append(f"SKILL.md missing {each_token}")
14
+ if errors:
15
+ print("PACKAGE INVALID")
16
+ print("\n".join(errors))
17
+ sys.exit(1)
18
+ print(f"PACKAGE VALID: {len(required)} required files present")
@@ -0,0 +1,4 @@
1
+ <h2 class="sr-only">Interactive visual summary.</h2>
2
+ <section aria-label="Visual widget">
3
+ <button type="button" aria-pressed="false">Show detail</button>
4
+ </section>
@@ -0,0 +1,19 @@
1
+ <svg width="100%" viewBox="0 0 680 180" role="img">
2
+ <title>Accessible visual title</title>
3
+ <desc>One-sentence description of the visual.</desc>
4
+ <defs>
5
+ <marker id="arrow" viewBox="0 0 10 10" refX="8" refY="5" markerUnits="userSpaceOnUse" markerWidth="6" markerHeight="6" orient="auto">
6
+ <path d="M2 1L8 5L2 9" fill="none" stroke="#3A3A3A" stroke-width="1.5" />
7
+ </marker>
8
+ </defs>
9
+ <rect x="0" y="0" width="680" height="180" fill="#F2EFE6" />
10
+ <g stroke-width="1">
11
+ <rect x="40" y="40" width="180" height="44" rx="8" fill="#FFFFFF" stroke="#9A9A9A" />
12
+ <line x1="220" y1="62" x2="300" y2="62" fill="none" stroke="#3A3A3A" marker-end="url(#arrow)" />
13
+ <rect x="300" y="40" width="180" height="44" rx="8" fill="#DFF0E4" stroke="#1E8449" />
14
+ </g>
15
+ <g fill="#1A1A1A" font-family="sans-serif" font-size="16" text-anchor="middle">
16
+ <text x="130" y="62" dominant-baseline="central">Start</text>
17
+ <text x="390" y="62" dominant-baseline="central">Next step</text>
18
+ </g>
19
+ </svg>
@@ -0,0 +1,6 @@
1
+ <svg width="100%" viewBox="0 0 680 120" role="img">
2
+ <title>Variable fill</title>
3
+ <desc>A labelled box whose text fill comes from a CSS custom property.</desc>
4
+ <g fill="none" stroke="currentColor" stroke-width="1"><rect x="40" y="40" width="240" height="44" rx="8"/></g>
5
+ <g fill="var(--ink)" font-family="sans-serif" font-size="16"><text x="60" y="68">Label</text></g>
6
+ </svg>
@@ -0,0 +1,7 @@
1
+ <svg width="100%" viewBox="0 0 680 120" role="img">
2
+ <title>Dead reference</title>
3
+ <desc>A use element pointing at a symbol the file never defines.</desc>
4
+ <g fill="none" stroke="currentColor" stroke-width="1"><rect x="40" y="40" width="240" height="44" rx="8"/></g>
5
+ <use href="#ghost" x="320" y="40"/>
6
+ <g fill="currentColor" font-family="sans-serif" font-size="16"><text x="60" y="68">Label</text></g>
7
+ </svg>
@@ -0,0 +1,8 @@
1
+ <svg width="100%" viewBox="0 0 680 140" role="img">
2
+ <title>Filled glyph</title>
3
+ <desc>A solid check mark drawn as a filled path beside its label.</desc>
4
+ <path d="M40 70L60 90L100 46Z" fill="currentColor"/>
5
+ <g fill="currentColor" font-family="sans-serif" font-size="16">
6
+ <text x="120" y="76">Done</text>
7
+ </g>
8
+ </svg>
@@ -0,0 +1,18 @@
1
+ <svg width="100%" viewBox="0 0 680 140" role="img">
2
+ <title>Inherited connector fill</title>
3
+ <desc>Two boxes joined by a connector that takes its fill from the surrounding group.</desc>
4
+ <defs>
5
+ <marker id="arrow" viewBox="0 0 10 10" refX="8" refY="5" markerUnits="userSpaceOnUse" markerWidth="6" markerHeight="6" orient="auto">
6
+ <path d="M2 1L8 5L2 9" fill="none" stroke="currentColor" stroke-width="1.5"/>
7
+ </marker>
8
+ </defs>
9
+ <g fill="none" stroke="currentColor" stroke-width="1">
10
+ <rect x="40" y="48" width="200" height="44" rx="8"/>
11
+ <line x1="240" y1="70" x2="320" y2="70" marker-end="url(#arrow)"/>
12
+ <rect x="320" y="48" width="200" height="44" rx="8"/>
13
+ </g>
14
+ <g fill="currentColor" font-family="sans-serif" font-size="16" text-anchor="middle">
15
+ <text x="140" y="70" dominant-baseline="central">Start</text>
16
+ <text x="420" y="70" dominant-baseline="central">End</text>
17
+ </g>
18
+ </svg>
@@ -0,0 +1 @@
1
+ <svg viewBox="0 0 300 100"><text font-size="8">Too small</text></svg>
@@ -0,0 +1,21 @@
1
+ <svg width="100%" viewBox="0 0 1760 640" role="img">
2
+ <title>Large canvas review flow</title>
3
+ <desc>A three-step flow across a large infographic canvas, from request to review to result.</desc>
4
+ <defs>
5
+ <marker id="arrow" viewBox="0 0 10 10" refX="8" refY="5" markerUnits="userSpaceOnUse" markerWidth="16" markerHeight="16" orient="auto">
6
+ <path d="M2 1L8 5L2 9" fill="none" stroke="currentColor" stroke-width="1.5"/>
7
+ </marker>
8
+ </defs>
9
+ <g fill="none" stroke="currentColor" stroke-width="2">
10
+ <rect x="80" y="240" width="440" height="120" rx="20"/>
11
+ <line x1="520" y1="300" x2="660" y2="300" fill="none" marker-end="url(#arrow)"/>
12
+ <rect x="660" y="240" width="440" height="120" rx="20"/>
13
+ <line x1="1100" y1="300" x2="1240" y2="300" fill="none" marker-end="url(#arrow)"/>
14
+ <rect x="1240" y="240" width="440" height="120" rx="20"/>
15
+ </g>
16
+ <g fill="currentColor" font-family="sans-serif" font-size="41" text-anchor="middle">
17
+ <text x="300" y="300" dominant-baseline="central">Request</text>
18
+ <text x="880" y="300" dominant-baseline="central">Review</text>
19
+ <text x="1460" y="300" dominant-baseline="central">Result</text>
20
+ </g>
21
+ </svg>
@@ -0,0 +1,15 @@
1
+ <svg width="100%" viewBox="0 0 680 140" role="img">
2
+ <title>Connector carrying a solid fill</title>
3
+ <desc>An arrow whose own fill is a solid colour rather than none.</desc>
4
+ <defs>
5
+ <marker id="arrow" viewBox="0 0 10 10" refX="8" refY="5" markerUnits="userSpaceOnUse" markerWidth="6" markerHeight="6" orient="auto">
6
+ <path d="M2 1L8 5L2 9" fill="none" stroke="currentColor" stroke-width="1.5"/>
7
+ </marker>
8
+ </defs>
9
+ <g stroke="currentColor" stroke-width="1">
10
+ <line x1="240" y1="70" x2="320" y2="70" fill="currentColor" marker-end="url(#arrow)"/>
11
+ </g>
12
+ <g fill="currentColor" font-family="sans-serif" font-size="16">
13
+ <text x="40" y="76">Label</text>
14
+ </g>
15
+ </svg>
@@ -0,0 +1 @@
1
+ <h2 class="sr-only">A valid visual.</h2><section aria-label="Visual"></section>
@@ -0,0 +1,74 @@
1
+ import subprocess
2
+ import sys
3
+ from pathlib import Path
4
+
5
+ ROOT = Path(__file__).parents[1]
6
+ SCRIPT = ROOT / "scripts" / "validate-artifact.py"
7
+ HEADER = (
8
+ '<svg width="100%" viewBox="0 0 680 120" role="img"><title>T</title><desc>D</desc>'
9
+ )
10
+
11
+
12
+ def check(artifact: Path) -> subprocess.CompletedProcess[str]:
13
+ return subprocess.run(
14
+ [sys.executable, str(SCRIPT), str(artifact)], text=True, capture_output=True
15
+ )
16
+
17
+
18
+ def write_svg(directory: Path, name: str, body: str) -> Path:
19
+ artifact = directory / name
20
+ artifact.write_text(body, encoding="utf-8")
21
+ return artifact
22
+
23
+
24
+ def test_text_floor_reports_tiny_text_when_the_viewbox_is_unreadable(
25
+ tmp_path: Path,
26
+ ) -> None:
27
+ artifact = write_svg(
28
+ tmp_path,
29
+ "no-viewbox.svg",
30
+ '<svg role="img"><title>T</title><desc>D</desc><text font-size="4">tiny</text></svg>',
31
+ )
32
+ finished = check(artifact)
33
+ assert "text below" in finished.stdout, finished.stdout + finished.stderr
34
+
35
+
36
+ def test_commented_out_connector_is_not_reported(tmp_path: Path) -> None:
37
+ artifact = write_svg(
38
+ tmp_path,
39
+ "commented.svg",
40
+ f'{HEADER}\n<!-- <line fill="red" marker-end="url(#a)"/> -->\n</svg>',
41
+ )
42
+ finished = check(artifact)
43
+ assert finished.returncode == 0, finished.stdout + finished.stderr
44
+
45
+
46
+ def test_live_connector_after_a_comment_is_still_reported(tmp_path: Path) -> None:
47
+ artifact = write_svg(
48
+ tmp_path,
49
+ "after-comment.svg",
50
+ f"{HEADER}\n<!-- an explanatory note -->\n"
51
+ '<defs><marker id="a" markerUnits="userSpaceOnUse"><path d="M2 1L8 5"/></marker></defs>\n'
52
+ '<line x1="10" y1="10" x2="90" y2="10" fill="red" marker-end="url(#a)"/>\n</svg>',
53
+ )
54
+ finished = check(artifact)
55
+ assert "connectors need fill=none" in finished.stdout, (
56
+ finished.stdout + finished.stderr
57
+ )
58
+
59
+
60
+ def test_connector_error_names_the_offending_tag(tmp_path: Path) -> None:
61
+ artifact = write_svg(
62
+ tmp_path,
63
+ "named.svg",
64
+ f"{HEADER}\n"
65
+ '<defs><marker id="a" markerUnits="userSpaceOnUse"><path d="M2 1L8 5"/></marker></defs>\n'
66
+ '<line x1="10" y1="10" x2="90" y2="10" fill="red" marker-end="url(#a)"/>\n</svg>',
67
+ )
68
+ finished = check(artifact)
69
+ connector_error = [
70
+ line for line in finished.stdout.splitlines() if "fill=none" in line
71
+ ]
72
+ assert connector_error and "line" in connector_error[0], (
73
+ finished.stdout + finished.stderr
74
+ )
@@ -0,0 +1,59 @@
1
+ import subprocess
2
+ import sys
3
+ from pathlib import Path
4
+
5
+ ROOT = Path(__file__).parents[1]
6
+
7
+ def run(*args: str) -> subprocess.CompletedProcess[str]:
8
+ return subprocess.run([sys.executable, *args], cwd=ROOT, text=True, capture_output=True)
9
+
10
+
11
+ def test_package() -> None:
12
+ finished = run("scripts/validate-package.py")
13
+ assert finished.returncode == 0, finished.stdout + finished.stderr
14
+
15
+
16
+ def test_template_svg() -> None:
17
+ finished = run("scripts/validate-artifact.py", "templates/svg-base.svg")
18
+ assert finished.returncode == 0, finished.stdout + finished.stderr
19
+
20
+
21
+ def test_valid_html_passes() -> None:
22
+ finished = run("scripts/validate-artifact.py", "tests/fixtures/valid.html")
23
+ assert finished.returncode == 0, finished.stdout + finished.stderr
24
+
25
+
26
+ def test_invalid_fixture_fails() -> None:
27
+ finished = run("scripts/validate-artifact.py", "tests/fixtures/invalid.svg")
28
+ assert finished.returncode != 0, finished.stdout + finished.stderr
29
+
30
+
31
+ def test_css_variable_fixture_fails() -> None:
32
+ finished = run("scripts/validate-artifact.py", "tests/fixtures/css-var.svg")
33
+ assert finished.returncode != 0, finished.stdout + finished.stderr
34
+
35
+
36
+ def test_dead_reference_fixture_fails() -> None:
37
+ finished = run("scripts/validate-artifact.py", "tests/fixtures/dead-ref.svg")
38
+ assert finished.returncode != 0, finished.stdout + finished.stderr
39
+
40
+
41
+ def test_large_canvas_fixture_passes() -> None:
42
+ finished = run("scripts/validate-artifact.py", "tests/fixtures/large-canvas.svg")
43
+ assert finished.returncode == 0, finished.stdout + finished.stderr
44
+
45
+
46
+ def test_connector_inheriting_group_fill_passes() -> None:
47
+ finished = run("scripts/validate-artifact.py", "tests/fixtures/inherited-fill.svg")
48
+ assert finished.returncode == 0, finished.stdout + finished.stderr
49
+
50
+
51
+ def test_filled_glyph_without_markers_passes() -> None:
52
+ finished = run("scripts/validate-artifact.py", "tests/fixtures/filled-glyph.svg")
53
+ assert finished.returncode == 0, finished.stdout + finished.stderr
54
+
55
+
56
+ def test_connector_with_solid_fill_fails() -> None:
57
+ finished = run("scripts/validate-artifact.py", "tests/fixtures/unfilled-connector.svg")
58
+ assert finished.returncode != 0, finished.stdout + finished.stderr
59
+ assert "connectors need fill=none" in finished.stdout