@oriro/orirocli 0.1.7 → 0.1.9

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 (351) hide show
  1. package/ATTRIBUTION.md +8 -0
  2. package/LICENSE +21 -0
  3. package/dist/cli.js +35 -5
  4. package/package.json +1 -1
  5. package/skills/21stdev/SKILL.md +64 -0
  6. package/skills/graphify/SKILL.md +619 -0
  7. package/skills/graphify/__init__.py +28 -0
  8. package/skills/graphify/__main__.py +4582 -0
  9. package/skills/graphify/affected.py +154 -0
  10. package/skills/graphify/always_on/agents-md.md +12 -0
  11. package/skills/graphify/always_on/antigravity-rules.md +14 -0
  12. package/skills/graphify/always_on/claude-md.md +9 -0
  13. package/skills/graphify/always_on/gemini-md.md +9 -0
  14. package/skills/graphify/always_on/kiro-steering.md +5 -0
  15. package/skills/graphify/always_on/vscode-instructions.md +17 -0
  16. package/skills/graphify/analyze.py +724 -0
  17. package/skills/graphify/benchmark.py +155 -0
  18. package/skills/graphify/build.py +487 -0
  19. package/skills/graphify/cache.py +417 -0
  20. package/skills/graphify/callflow_html.py +2020 -0
  21. package/skills/graphify/cluster.py +272 -0
  22. package/skills/graphify/command-kilo.md +15 -0
  23. package/skills/graphify/dedup.py +429 -0
  24. package/skills/graphify/detect.py +1379 -0
  25. package/skills/graphify/diagnostics.py +390 -0
  26. package/skills/graphify/export.py +1408 -0
  27. package/skills/graphify/extract.py +11570 -0
  28. package/skills/graphify/global_graph.py +159 -0
  29. package/skills/graphify/google_workspace.py +223 -0
  30. package/skills/graphify/hooks.py +457 -0
  31. package/skills/graphify/ingest.py +331 -0
  32. package/skills/graphify/llm.py +1896 -0
  33. package/skills/graphify/manifest.py +4 -0
  34. package/skills/graphify/mcp_ingest.py +392 -0
  35. package/skills/graphify/multigraph_compat.py +212 -0
  36. package/skills/graphify/pg_introspect.py +142 -0
  37. package/skills/graphify/prs.py +748 -0
  38. package/skills/graphify/querylog.py +70 -0
  39. package/skills/graphify/report.py +218 -0
  40. package/skills/graphify/scip_ingest.py +363 -0
  41. package/skills/graphify/security.py +336 -0
  42. package/skills/graphify/semantic_cleanup.py +319 -0
  43. package/skills/graphify/serve.py +1309 -0
  44. package/skills/graphify/skill-aider.md +1246 -0
  45. package/skills/graphify/skill-amp.md +613 -0
  46. package/skills/graphify/skill-claw.md +616 -0
  47. package/skills/graphify/skill-codex.md +613 -0
  48. package/skills/graphify/skill-copilot.md +616 -0
  49. package/skills/graphify/skill-devin.md +1372 -0
  50. package/skills/graphify/skill-droid.md +613 -0
  51. package/skills/graphify/skill-kilo.md +625 -0
  52. package/skills/graphify/skill-kiro.md +615 -0
  53. package/skills/graphify/skill-opencode.md +608 -0
  54. package/skills/graphify/skill-pi.md +615 -0
  55. package/skills/graphify/skill-trae.md +614 -0
  56. package/skills/graphify/skill-vscode.md +612 -0
  57. package/skills/graphify/skill-windows.md +651 -0
  58. package/skills/graphify/skills/amp/references/add-watch.md +56 -0
  59. package/skills/graphify/skills/amp/references/exports.md +71 -0
  60. package/skills/graphify/skills/amp/references/extraction-spec.md +68 -0
  61. package/skills/graphify/skills/amp/references/github-and-merge.md +46 -0
  62. package/skills/graphify/skills/amp/references/hooks.md +33 -0
  63. package/skills/graphify/skills/amp/references/query.md +249 -0
  64. package/skills/graphify/skills/amp/references/transcribe.md +48 -0
  65. package/skills/graphify/skills/amp/references/update.md +179 -0
  66. package/skills/graphify/skills/claude/references/add-watch.md +56 -0
  67. package/skills/graphify/skills/claude/references/exports.md +71 -0
  68. package/skills/graphify/skills/claude/references/extraction-spec.md +68 -0
  69. package/skills/graphify/skills/claude/references/github-and-merge.md +46 -0
  70. package/skills/graphify/skills/claude/references/hooks.md +33 -0
  71. package/skills/graphify/skills/claude/references/query.md +103 -0
  72. package/skills/graphify/skills/claude/references/transcribe.md +48 -0
  73. package/skills/graphify/skills/claude/references/update.md +179 -0
  74. package/skills/graphify/skills/claw/references/add-watch.md +56 -0
  75. package/skills/graphify/skills/claw/references/exports.md +71 -0
  76. package/skills/graphify/skills/claw/references/extraction-spec.md +29 -0
  77. package/skills/graphify/skills/claw/references/github-and-merge.md +46 -0
  78. package/skills/graphify/skills/claw/references/hooks.md +33 -0
  79. package/skills/graphify/skills/claw/references/query.md +249 -0
  80. package/skills/graphify/skills/claw/references/transcribe.md +48 -0
  81. package/skills/graphify/skills/claw/references/update.md +179 -0
  82. package/skills/graphify/skills/codex/references/add-watch.md +56 -0
  83. package/skills/graphify/skills/codex/references/exports.md +71 -0
  84. package/skills/graphify/skills/codex/references/extraction-spec.md +29 -0
  85. package/skills/graphify/skills/codex/references/github-and-merge.md +46 -0
  86. package/skills/graphify/skills/codex/references/hooks.md +33 -0
  87. package/skills/graphify/skills/codex/references/query.md +249 -0
  88. package/skills/graphify/skills/codex/references/transcribe.md +48 -0
  89. package/skills/graphify/skills/codex/references/update.md +179 -0
  90. package/skills/graphify/skills/copilot/references/add-watch.md +56 -0
  91. package/skills/graphify/skills/copilot/references/exports.md +71 -0
  92. package/skills/graphify/skills/copilot/references/extraction-spec.md +68 -0
  93. package/skills/graphify/skills/copilot/references/github-and-merge.md +46 -0
  94. package/skills/graphify/skills/copilot/references/hooks.md +33 -0
  95. package/skills/graphify/skills/copilot/references/query.md +249 -0
  96. package/skills/graphify/skills/copilot/references/transcribe.md +48 -0
  97. package/skills/graphify/skills/copilot/references/update.md +179 -0
  98. package/skills/graphify/skills/droid/references/add-watch.md +56 -0
  99. package/skills/graphify/skills/droid/references/exports.md +71 -0
  100. package/skills/graphify/skills/droid/references/extraction-spec.md +68 -0
  101. package/skills/graphify/skills/droid/references/github-and-merge.md +46 -0
  102. package/skills/graphify/skills/droid/references/hooks.md +33 -0
  103. package/skills/graphify/skills/droid/references/query.md +249 -0
  104. package/skills/graphify/skills/droid/references/transcribe.md +48 -0
  105. package/skills/graphify/skills/droid/references/update.md +179 -0
  106. package/skills/graphify/skills/kilo/references/add-watch.md +56 -0
  107. package/skills/graphify/skills/kilo/references/exports.md +71 -0
  108. package/skills/graphify/skills/kilo/references/extraction-spec.md +68 -0
  109. package/skills/graphify/skills/kilo/references/github-and-merge.md +46 -0
  110. package/skills/graphify/skills/kilo/references/hooks.md +33 -0
  111. package/skills/graphify/skills/kilo/references/query.md +249 -0
  112. package/skills/graphify/skills/kilo/references/transcribe.md +48 -0
  113. package/skills/graphify/skills/kilo/references/update.md +179 -0
  114. package/skills/graphify/skills/kiro/references/add-watch.md +56 -0
  115. package/skills/graphify/skills/kiro/references/exports.md +71 -0
  116. package/skills/graphify/skills/kiro/references/extraction-spec.md +29 -0
  117. package/skills/graphify/skills/kiro/references/github-and-merge.md +46 -0
  118. package/skills/graphify/skills/kiro/references/hooks.md +33 -0
  119. package/skills/graphify/skills/kiro/references/query.md +249 -0
  120. package/skills/graphify/skills/kiro/references/transcribe.md +48 -0
  121. package/skills/graphify/skills/kiro/references/update.md +179 -0
  122. package/skills/graphify/skills/opencode/references/add-watch.md +56 -0
  123. package/skills/graphify/skills/opencode/references/exports.md +71 -0
  124. package/skills/graphify/skills/opencode/references/extraction-spec.md +68 -0
  125. package/skills/graphify/skills/opencode/references/github-and-merge.md +46 -0
  126. package/skills/graphify/skills/opencode/references/hooks.md +33 -0
  127. package/skills/graphify/skills/opencode/references/query.md +249 -0
  128. package/skills/graphify/skills/opencode/references/transcribe.md +48 -0
  129. package/skills/graphify/skills/opencode/references/update.md +179 -0
  130. package/skills/graphify/skills/pi/references/add-watch.md +56 -0
  131. package/skills/graphify/skills/pi/references/exports.md +71 -0
  132. package/skills/graphify/skills/pi/references/extraction-spec.md +29 -0
  133. package/skills/graphify/skills/pi/references/github-and-merge.md +46 -0
  134. package/skills/graphify/skills/pi/references/hooks.md +33 -0
  135. package/skills/graphify/skills/pi/references/query.md +249 -0
  136. package/skills/graphify/skills/pi/references/transcribe.md +48 -0
  137. package/skills/graphify/skills/pi/references/update.md +179 -0
  138. package/skills/graphify/skills/trae/references/add-watch.md +56 -0
  139. package/skills/graphify/skills/trae/references/exports.md +71 -0
  140. package/skills/graphify/skills/trae/references/extraction-spec.md +68 -0
  141. package/skills/graphify/skills/trae/references/github-and-merge.md +46 -0
  142. package/skills/graphify/skills/trae/references/hooks.md +35 -0
  143. package/skills/graphify/skills/trae/references/query.md +249 -0
  144. package/skills/graphify/skills/trae/references/transcribe.md +48 -0
  145. package/skills/graphify/skills/trae/references/update.md +179 -0
  146. package/skills/graphify/skills/vscode/references/add-watch.md +56 -0
  147. package/skills/graphify/skills/vscode/references/exports.md +71 -0
  148. package/skills/graphify/skills/vscode/references/extraction-spec.md +68 -0
  149. package/skills/graphify/skills/vscode/references/github-and-merge.md +46 -0
  150. package/skills/graphify/skills/vscode/references/hooks.md +33 -0
  151. package/skills/graphify/skills/vscode/references/query.md +249 -0
  152. package/skills/graphify/skills/vscode/references/transcribe.md +48 -0
  153. package/skills/graphify/skills/vscode/references/update.md +179 -0
  154. package/skills/graphify/skills/windows/references/add-watch.md +56 -0
  155. package/skills/graphify/skills/windows/references/exports.md +71 -0
  156. package/skills/graphify/skills/windows/references/extraction-spec.md +68 -0
  157. package/skills/graphify/skills/windows/references/github-and-merge.md +46 -0
  158. package/skills/graphify/skills/windows/references/hooks.md +33 -0
  159. package/skills/graphify/skills/windows/references/query.md +249 -0
  160. package/skills/graphify/skills/windows/references/transcribe.md +48 -0
  161. package/skills/graphify/skills/windows/references/update.md +179 -0
  162. package/skills/graphify/symbol_resolution.py +538 -0
  163. package/skills/graphify/transcribe.py +184 -0
  164. package/skills/graphify/tree_html.py +582 -0
  165. package/skills/graphify/validate.py +72 -0
  166. package/skills/graphify/watch.py +898 -0
  167. package/skills/graphify/wiki.py +282 -0
  168. package/skills/impeccable/SKILL.md +186 -0
  169. package/skills/impeccable/agents/impeccable_asset_producer.toml +92 -0
  170. package/skills/impeccable/agents/impeccable_manual_edit_applier.toml +95 -0
  171. package/skills/impeccable/agents/openai.yaml +4 -0
  172. package/skills/impeccable/reference/adapt.md +311 -0
  173. package/skills/impeccable/reference/animate.md +201 -0
  174. package/skills/impeccable/reference/audit.md +133 -0
  175. package/skills/impeccable/reference/bolder.md +113 -0
  176. package/skills/impeccable/reference/brand.md +108 -0
  177. package/skills/impeccable/reference/clarify.md +288 -0
  178. package/skills/impeccable/reference/codex.md +105 -0
  179. package/skills/impeccable/reference/colorize.md +257 -0
  180. package/skills/impeccable/reference/craft.md +123 -0
  181. package/skills/impeccable/reference/critique.md +790 -0
  182. package/skills/impeccable/reference/delight.md +302 -0
  183. package/skills/impeccable/reference/distill.md +111 -0
  184. package/skills/impeccable/reference/document.md +429 -0
  185. package/skills/impeccable/reference/extract.md +69 -0
  186. package/skills/impeccable/reference/harden.md +347 -0
  187. package/skills/impeccable/reference/init.md +172 -0
  188. package/skills/impeccable/reference/interaction-design.md +189 -0
  189. package/skills/impeccable/reference/layout.md +161 -0
  190. package/skills/impeccable/reference/live.md +720 -0
  191. package/skills/impeccable/reference/onboard.md +234 -0
  192. package/skills/impeccable/reference/optimize.md +258 -0
  193. package/skills/impeccable/reference/overdrive.md +130 -0
  194. package/skills/impeccable/reference/polish.md +241 -0
  195. package/skills/impeccable/reference/product.md +60 -0
  196. package/skills/impeccable/reference/quieter.md +99 -0
  197. package/skills/impeccable/reference/shape.md +165 -0
  198. package/skills/impeccable/reference/typeset.md +279 -0
  199. package/skills/impeccable/scripts/cleanup-deprecated.mjs +284 -0
  200. package/skills/impeccable/scripts/command-metadata.json +94 -0
  201. package/skills/impeccable/scripts/context-signals.mjs +225 -0
  202. package/skills/impeccable/scripts/context.mjs +266 -0
  203. package/skills/impeccable/scripts/critique-storage.mjs +242 -0
  204. package/skills/impeccable/scripts/design-parser.mjs +835 -0
  205. package/skills/impeccable/scripts/detect-csp.mjs +198 -0
  206. package/skills/impeccable/scripts/detect.mjs +21 -0
  207. package/skills/impeccable/scripts/detector/browser/injected/index.mjs +1733 -0
  208. package/skills/impeccable/scripts/detector/cli/main.mjs +244 -0
  209. package/skills/impeccable/scripts/detector/detect-antipatterns-browser.js +4618 -0
  210. package/skills/impeccable/scripts/detector/detect-antipatterns.mjs +43 -0
  211. package/skills/impeccable/scripts/detector/engines/browser/detect-url.mjs +252 -0
  212. package/skills/impeccable/scripts/detector/engines/regex/detect-text.mjs +535 -0
  213. package/skills/impeccable/scripts/detector/engines/static-html/css-cascade.mjs +986 -0
  214. package/skills/impeccable/scripts/detector/engines/static-html/detect-html.mjs +208 -0
  215. package/skills/impeccable/scripts/detector/engines/visual/screenshot-contrast.mjs +189 -0
  216. package/skills/impeccable/scripts/detector/findings.mjs +12 -0
  217. package/skills/impeccable/scripts/detector/node/file-system.mjs +198 -0
  218. package/skills/impeccable/scripts/detector/profile/profiler.mjs +166 -0
  219. package/skills/impeccable/scripts/detector/registry/antipatterns.mjs +419 -0
  220. package/skills/impeccable/scripts/detector/rules/checks.mjs +2384 -0
  221. package/skills/impeccable/scripts/detector/shared/color.mjs +124 -0
  222. package/skills/impeccable/scripts/detector/shared/constants.mjs +101 -0
  223. package/skills/impeccable/scripts/detector/shared/page.mjs +7 -0
  224. package/skills/impeccable/scripts/impeccable-paths.mjs +126 -0
  225. package/skills/impeccable/scripts/is-generated.mjs +69 -0
  226. package/skills/impeccable/scripts/live-accept.mjs +812 -0
  227. package/skills/impeccable/scripts/live-browser-session.js +123 -0
  228. package/skills/impeccable/scripts/live-browser.js +10295 -0
  229. package/skills/impeccable/scripts/live-commit-manual-edits.mjs +1241 -0
  230. package/skills/impeccable/scripts/live-complete.mjs +75 -0
  231. package/skills/impeccable/scripts/live-completion.mjs +19 -0
  232. package/skills/impeccable/scripts/live-copy-edit-agent.mjs +683 -0
  233. package/skills/impeccable/scripts/live-discard-manual-edits.mjs +51 -0
  234. package/skills/impeccable/scripts/live-event-validation.mjs +137 -0
  235. package/skills/impeccable/scripts/live-inject.mjs +557 -0
  236. package/skills/impeccable/scripts/live-insert-ui.mjs +458 -0
  237. package/skills/impeccable/scripts/live-insert.mjs +272 -0
  238. package/skills/impeccable/scripts/live-manual-edit-evidence.mjs +363 -0
  239. package/skills/impeccable/scripts/live-manual-edits-buffer.mjs +152 -0
  240. package/skills/impeccable/scripts/live-poll.mjs +379 -0
  241. package/skills/impeccable/scripts/live-resume.mjs +94 -0
  242. package/skills/impeccable/scripts/live-server.mjs +2326 -0
  243. package/skills/impeccable/scripts/live-session-store.mjs +289 -0
  244. package/skills/impeccable/scripts/live-status.mjs +61 -0
  245. package/skills/impeccable/scripts/live-svelte-component.mjs +826 -0
  246. package/skills/impeccable/scripts/live-sveltekit-adapter.mjs +274 -0
  247. package/skills/impeccable/scripts/live-ui-core.mjs +179 -0
  248. package/skills/impeccable/scripts/live-vocabulary.mjs +36 -0
  249. package/skills/impeccable/scripts/live-wrap.mjs +894 -0
  250. package/skills/impeccable/scripts/live.mjs +246 -0
  251. package/skills/impeccable/scripts/modern-screenshot.umd.js +14 -0
  252. package/skills/impeccable/scripts/palette.mjs +633 -0
  253. package/skills/impeccable/scripts/pin.mjs +214 -0
  254. package/skills/uipm-ui-styling/LICENSE.txt +202 -0
  255. package/skills/uipm-ui-styling/SKILL.md +328 -0
  256. package/skills/uipm-ui-styling/canvas-fonts/ArsenalSC-OFL.txt +93 -0
  257. package/skills/uipm-ui-styling/canvas-fonts/ArsenalSC-Regular.ttf +0 -0
  258. package/skills/uipm-ui-styling/canvas-fonts/BigShoulders-Bold.ttf +0 -0
  259. package/skills/uipm-ui-styling/canvas-fonts/BigShoulders-OFL.txt +93 -0
  260. package/skills/uipm-ui-styling/canvas-fonts/BigShoulders-Regular.ttf +0 -0
  261. package/skills/uipm-ui-styling/canvas-fonts/Boldonse-OFL.txt +93 -0
  262. package/skills/uipm-ui-styling/canvas-fonts/Boldonse-Regular.ttf +0 -0
  263. package/skills/uipm-ui-styling/canvas-fonts/BricolageGrotesque-Bold.ttf +0 -0
  264. package/skills/uipm-ui-styling/canvas-fonts/BricolageGrotesque-OFL.txt +93 -0
  265. package/skills/uipm-ui-styling/canvas-fonts/BricolageGrotesque-Regular.ttf +0 -0
  266. package/skills/uipm-ui-styling/canvas-fonts/CrimsonPro-Bold.ttf +0 -0
  267. package/skills/uipm-ui-styling/canvas-fonts/CrimsonPro-Italic.ttf +0 -0
  268. package/skills/uipm-ui-styling/canvas-fonts/CrimsonPro-OFL.txt +93 -0
  269. package/skills/uipm-ui-styling/canvas-fonts/CrimsonPro-Regular.ttf +0 -0
  270. package/skills/uipm-ui-styling/canvas-fonts/DMMono-OFL.txt +93 -0
  271. package/skills/uipm-ui-styling/canvas-fonts/DMMono-Regular.ttf +0 -0
  272. package/skills/uipm-ui-styling/canvas-fonts/EricaOne-OFL.txt +94 -0
  273. package/skills/uipm-ui-styling/canvas-fonts/EricaOne-Regular.ttf +0 -0
  274. package/skills/uipm-ui-styling/canvas-fonts/GeistMono-Bold.ttf +0 -0
  275. package/skills/uipm-ui-styling/canvas-fonts/GeistMono-OFL.txt +93 -0
  276. package/skills/uipm-ui-styling/canvas-fonts/GeistMono-Regular.ttf +0 -0
  277. package/skills/uipm-ui-styling/canvas-fonts/Gloock-OFL.txt +93 -0
  278. package/skills/uipm-ui-styling/canvas-fonts/Gloock-Regular.ttf +0 -0
  279. package/skills/uipm-ui-styling/canvas-fonts/IBMPlexMono-Bold.ttf +0 -0
  280. package/skills/uipm-ui-styling/canvas-fonts/IBMPlexMono-OFL.txt +93 -0
  281. package/skills/uipm-ui-styling/canvas-fonts/IBMPlexMono-Regular.ttf +0 -0
  282. package/skills/uipm-ui-styling/canvas-fonts/IBMPlexSerif-Bold.ttf +0 -0
  283. package/skills/uipm-ui-styling/canvas-fonts/IBMPlexSerif-BoldItalic.ttf +0 -0
  284. package/skills/uipm-ui-styling/canvas-fonts/IBMPlexSerif-Italic.ttf +0 -0
  285. package/skills/uipm-ui-styling/canvas-fonts/IBMPlexSerif-Regular.ttf +0 -0
  286. package/skills/uipm-ui-styling/canvas-fonts/InstrumentSans-Bold.ttf +0 -0
  287. package/skills/uipm-ui-styling/canvas-fonts/InstrumentSans-BoldItalic.ttf +0 -0
  288. package/skills/uipm-ui-styling/canvas-fonts/InstrumentSans-Italic.ttf +0 -0
  289. package/skills/uipm-ui-styling/canvas-fonts/InstrumentSans-OFL.txt +93 -0
  290. package/skills/uipm-ui-styling/canvas-fonts/InstrumentSans-Regular.ttf +0 -0
  291. package/skills/uipm-ui-styling/canvas-fonts/InstrumentSerif-Italic.ttf +0 -0
  292. package/skills/uipm-ui-styling/canvas-fonts/InstrumentSerif-Regular.ttf +0 -0
  293. package/skills/uipm-ui-styling/canvas-fonts/Italiana-OFL.txt +93 -0
  294. package/skills/uipm-ui-styling/canvas-fonts/Italiana-Regular.ttf +0 -0
  295. package/skills/uipm-ui-styling/canvas-fonts/JetBrainsMono-Bold.ttf +0 -0
  296. package/skills/uipm-ui-styling/canvas-fonts/JetBrainsMono-OFL.txt +93 -0
  297. package/skills/uipm-ui-styling/canvas-fonts/JetBrainsMono-Regular.ttf +0 -0
  298. package/skills/uipm-ui-styling/canvas-fonts/Jura-Light.ttf +0 -0
  299. package/skills/uipm-ui-styling/canvas-fonts/Jura-Medium.ttf +0 -0
  300. package/skills/uipm-ui-styling/canvas-fonts/Jura-OFL.txt +93 -0
  301. package/skills/uipm-ui-styling/canvas-fonts/LibreBaskerville-OFL.txt +93 -0
  302. package/skills/uipm-ui-styling/canvas-fonts/LibreBaskerville-Regular.ttf +0 -0
  303. package/skills/uipm-ui-styling/canvas-fonts/Lora-Bold.ttf +0 -0
  304. package/skills/uipm-ui-styling/canvas-fonts/Lora-BoldItalic.ttf +0 -0
  305. package/skills/uipm-ui-styling/canvas-fonts/Lora-Italic.ttf +0 -0
  306. package/skills/uipm-ui-styling/canvas-fonts/Lora-OFL.txt +93 -0
  307. package/skills/uipm-ui-styling/canvas-fonts/Lora-Regular.ttf +0 -0
  308. package/skills/uipm-ui-styling/canvas-fonts/NationalPark-Bold.ttf +0 -0
  309. package/skills/uipm-ui-styling/canvas-fonts/NationalPark-OFL.txt +93 -0
  310. package/skills/uipm-ui-styling/canvas-fonts/NationalPark-Regular.ttf +0 -0
  311. package/skills/uipm-ui-styling/canvas-fonts/NothingYouCouldDo-OFL.txt +93 -0
  312. package/skills/uipm-ui-styling/canvas-fonts/NothingYouCouldDo-Regular.ttf +0 -0
  313. package/skills/uipm-ui-styling/canvas-fonts/Outfit-Bold.ttf +0 -0
  314. package/skills/uipm-ui-styling/canvas-fonts/Outfit-OFL.txt +93 -0
  315. package/skills/uipm-ui-styling/canvas-fonts/Outfit-Regular.ttf +0 -0
  316. package/skills/uipm-ui-styling/canvas-fonts/PixelifySans-Medium.ttf +0 -0
  317. package/skills/uipm-ui-styling/canvas-fonts/PixelifySans-OFL.txt +93 -0
  318. package/skills/uipm-ui-styling/canvas-fonts/PoiretOne-OFL.txt +93 -0
  319. package/skills/uipm-ui-styling/canvas-fonts/PoiretOne-Regular.ttf +0 -0
  320. package/skills/uipm-ui-styling/canvas-fonts/RedHatMono-Bold.ttf +0 -0
  321. package/skills/uipm-ui-styling/canvas-fonts/RedHatMono-OFL.txt +93 -0
  322. package/skills/uipm-ui-styling/canvas-fonts/RedHatMono-Regular.ttf +0 -0
  323. package/skills/uipm-ui-styling/canvas-fonts/Silkscreen-OFL.txt +93 -0
  324. package/skills/uipm-ui-styling/canvas-fonts/Silkscreen-Regular.ttf +0 -0
  325. package/skills/uipm-ui-styling/canvas-fonts/SmoochSans-Medium.ttf +0 -0
  326. package/skills/uipm-ui-styling/canvas-fonts/SmoochSans-OFL.txt +93 -0
  327. package/skills/uipm-ui-styling/canvas-fonts/Tektur-Medium.ttf +0 -0
  328. package/skills/uipm-ui-styling/canvas-fonts/Tektur-OFL.txt +93 -0
  329. package/skills/uipm-ui-styling/canvas-fonts/Tektur-Regular.ttf +0 -0
  330. package/skills/uipm-ui-styling/canvas-fonts/WorkSans-Bold.ttf +0 -0
  331. package/skills/uipm-ui-styling/canvas-fonts/WorkSans-BoldItalic.ttf +0 -0
  332. package/skills/uipm-ui-styling/canvas-fonts/WorkSans-Italic.ttf +0 -0
  333. package/skills/uipm-ui-styling/canvas-fonts/WorkSans-OFL.txt +93 -0
  334. package/skills/uipm-ui-styling/canvas-fonts/WorkSans-Regular.ttf +0 -0
  335. package/skills/uipm-ui-styling/canvas-fonts/YoungSerif-OFL.txt +93 -0
  336. package/skills/uipm-ui-styling/canvas-fonts/YoungSerif-Regular.ttf +0 -0
  337. package/skills/uipm-ui-styling/references/canvas-design-system.md +320 -0
  338. package/skills/uipm-ui-styling/references/shadcn-accessibility.md +471 -0
  339. package/skills/uipm-ui-styling/references/shadcn-components.md +424 -0
  340. package/skills/uipm-ui-styling/references/shadcn-theming.md +373 -0
  341. package/skills/uipm-ui-styling/references/tailwind-customization.md +483 -0
  342. package/skills/uipm-ui-styling/references/tailwind-responsive.md +382 -0
  343. package/skills/uipm-ui-styling/references/tailwind-utilities.md +455 -0
  344. package/skills/uipm-ui-styling/scripts/.coverage +0 -0
  345. package/skills/uipm-ui-styling/scripts/requirements.txt +17 -0
  346. package/skills/uipm-ui-styling/scripts/shadcn_add.py +292 -0
  347. package/skills/uipm-ui-styling/scripts/tailwind_config_gen.py +456 -0
  348. package/skills/uipm-ui-styling/scripts/tests/coverage-ui.json +1 -0
  349. package/skills/uipm-ui-styling/scripts/tests/requirements.txt +3 -0
  350. package/skills/uipm-ui-styling/scripts/tests/test_shadcn_add.py +266 -0
  351. package/skills/uipm-ui-styling/scripts/tests/test_tailwind_config_gen.py +336 -0
@@ -0,0 +1,4 @@
1
+ # re-export manifest helpers from detect for backwards compatibility
2
+ from graphify.detect import save_manifest, load_manifest, detect_incremental
3
+
4
+ __all__ = ["save_manifest", "load_manifest", "detect_incremental"]
@@ -0,0 +1,392 @@
1
+ """mcp_ingest.py — Extract MCP (Model Context Protocol) server configuration files.
2
+
3
+ Reads `.mcp.json` / `claude_desktop_config.json` / `mcp.json` / `mcp_servers.json`
4
+ and turns the `mcpServers` map into Graphify nodes and edges.
5
+
6
+ Symmetry with `serve.py`: Graphify exposes itself AS an MCP server. This module
7
+ indexes MCP servers AS a corpus type, completing the loop — an agent that runs
8
+ graphify with `--mcp` can now query its own configured MCP layer.
9
+
10
+ Entry point:
11
+ extract_mcp_config(path: Path) -> dict[str, list[dict]]
12
+
13
+ Returns `{"nodes": [...], "edges": [...]}` compatible with Graphify's
14
+ extraction-result format. Returns `{"nodes": [...], "edges": [...], "error": "..."}`
15
+ when the file is malformed, too large, or has no `mcpServers` map — the empty
16
+ result keeps it indistinguishable from "no MCP config here" for downstream
17
+ callers.
18
+
19
+ Detected filenames (case-sensitive, matched on basename):
20
+ - .mcp.json (Claude Code project config)
21
+ - claude_desktop_config.json (Claude Desktop)
22
+ - mcp.json (generic / per-tool)
23
+ - mcp_servers.json (alternate naming)
24
+
25
+ Schema emitted:
26
+ Node kinds:
27
+ - file the config file itself (label = filename)
28
+ - mcp_server one per entry under mcpServers
29
+ - mcp_command executable (npx, uvx, node, python, ...) — global ID
30
+ - mcp_package npm / pypi package id parsed from args — global ID
31
+ - env_var env variable NAME only — global ID. VALUES ARE NEVER READ.
32
+
33
+ Edge relations:
34
+ - contains file -> mcp_server
35
+ - references mcp_server -> mcp_command
36
+ - references mcp_server -> mcp_package
37
+ - requires_env mcp_server -> env_var (new relation; distinguishes
38
+ env dependencies from generic refs)
39
+
40
+ Security:
41
+ - Env var VALUES are never read, persisted, labelled, or surfaced. Only env
42
+ var NAMES become nodes. (`env: {"API_KEY": "sk-..."}` -> node "API_KEY" only.)
43
+ - File size capped at 1 MiB (matches extract_json).
44
+ - All labels go through `sanitize_label` (control characters stripped, length
45
+ capped) before emission.
46
+ - Args are NOT persisted as nodes/edges to avoid leaking paths or secrets that
47
+ some servers embed as positional args.
48
+
49
+ Cross-config emergent edges:
50
+ Because `mcp_command`, `mcp_package`, and `env_var` nodes use global IDs (no
51
+ per-file stem prefix), the same package or env var across two MCP configs
52
+ produces shared nodes — naturally surfacing "what configs depend on this
53
+ thing?" via graph traversal. Server nodes ARE stem-scoped so two configs
54
+ declaring different servers under the same key (e.g., both have "filesystem")
55
+ do not collide.
56
+ """
57
+
58
+ from __future__ import annotations
59
+
60
+ import json
61
+ import re
62
+ import unicodedata
63
+ from pathlib import Path
64
+ from typing import Any
65
+
66
+ from graphify.security import sanitize_label
67
+
68
+
69
+ MCP_CONFIG_FILENAMES: frozenset[str] = frozenset({
70
+ ".mcp.json",
71
+ "claude_desktop_config.json",
72
+ "mcp.json",
73
+ "mcp_servers.json",
74
+ })
75
+
76
+ _MAX_BYTES = 1_048_576 # 1 MiB — same cap as extract_json
77
+ _MAX_SERVERS_PER_FILE = 200 # generous; flags pathological configs
78
+
79
+
80
+ def is_mcp_config_path(path: Path) -> bool:
81
+ """Return True when ``path`` is a recognised MCP config filename."""
82
+ return path.name in MCP_CONFIG_FILENAMES
83
+
84
+
85
+ def extract_mcp_config(path: Path) -> dict[str, Any]:
86
+ """Parse an MCP config file into Graphify nodes and edges.
87
+
88
+ Behaviour matches other extractors in `extract.py`:
89
+ - returns ``{"nodes": [...], "edges": [...]}`` on success
90
+ - returns ``{"nodes": [], "edges": [], "error": "<reason>"}`` on parse
91
+ failure, oversize file, or missing ``mcpServers`` map
92
+ """
93
+ try:
94
+ with path.open("rb") as fh:
95
+ raw = fh.read(_MAX_BYTES + 1)
96
+ except OSError as exc:
97
+ return {"nodes": [], "edges": [], "error": f"mcp_ingest read error: {exc}"}
98
+
99
+ if len(raw) > _MAX_BYTES:
100
+ return {"nodes": [], "edges": [], "error": "mcp config too large to index"}
101
+
102
+ try:
103
+ text = raw.decode("utf-8")
104
+ except UnicodeDecodeError as exc:
105
+ return {"nodes": [], "edges": [], "error": f"mcp_ingest decode error: {exc}"}
106
+
107
+ try:
108
+ doc = json.loads(text)
109
+ except json.JSONDecodeError as exc:
110
+ return {"nodes": [], "edges": [], "error": f"mcp_ingest json error: {exc}"}
111
+
112
+ if not isinstance(doc, dict):
113
+ return {"nodes": [], "edges": [], "error": "mcp_ingest: root is not an object"}
114
+
115
+ servers = doc.get("mcpServers")
116
+ if not isinstance(servers, dict):
117
+ # Some tools nest the map (e.g., {"mcp": {"servers": {...}}}). Try one
118
+ # well-known alternate shape but do not search exhaustively.
119
+ nested = doc.get("mcp")
120
+ if isinstance(nested, dict):
121
+ servers = nested.get("servers")
122
+ if not isinstance(servers, dict):
123
+ return {"nodes": [], "edges": [], "error": "mcp_ingest: no mcpServers map"}
124
+
125
+ str_path = str(path)
126
+ file_nid = _make_id(str_path)
127
+ nodes: list[dict[str, Any]] = []
128
+ edges: list[dict[str, Any]] = []
129
+ seen_node_ids: set[str] = set()
130
+ seen_edge_keys: set[tuple[str, str, str]] = set()
131
+
132
+ _add_node(
133
+ nodes, seen_node_ids,
134
+ nid=file_nid,
135
+ label=path.name,
136
+ kind="mcp_config_file",
137
+ source_file=str_path,
138
+ line=1,
139
+ )
140
+
141
+ file_stem = _file_stem(path)
142
+ server_count = 0
143
+ for server_name, spec in servers.items():
144
+ if not isinstance(server_name, str) or not server_name:
145
+ continue
146
+ if not isinstance(spec, dict):
147
+ # Skip non-object server entries silently — the broken entry is
148
+ # the user's, not ours.
149
+ continue
150
+ if server_count >= _MAX_SERVERS_PER_FILE:
151
+ break
152
+ server_count += 1
153
+ _emit_server(
154
+ server_name=server_name,
155
+ spec=spec,
156
+ file_nid=file_nid,
157
+ file_stem=file_stem,
158
+ source_file=str_path,
159
+ nodes=nodes,
160
+ edges=edges,
161
+ seen_node_ids=seen_node_ids,
162
+ seen_edge_keys=seen_edge_keys,
163
+ )
164
+
165
+ return {"nodes": nodes, "edges": edges}
166
+
167
+
168
+ def _emit_server(
169
+ *,
170
+ server_name: str,
171
+ spec: dict[str, Any],
172
+ file_nid: str,
173
+ file_stem: str,
174
+ source_file: str,
175
+ nodes: list[dict[str, Any]],
176
+ edges: list[dict[str, Any]],
177
+ seen_node_ids: set[str],
178
+ seen_edge_keys: set[tuple[str, str, str]],
179
+ ) -> None:
180
+ """Emit nodes/edges for one entry under ``mcpServers``."""
181
+ server_nid = _make_id(file_stem, "mcp_server", server_name)
182
+ _add_node(
183
+ nodes, seen_node_ids,
184
+ nid=server_nid,
185
+ label=server_name,
186
+ kind="mcp_server",
187
+ source_file=source_file,
188
+ line=1, # JSON doesn't expose line numbers without a parser pass
189
+ )
190
+ _add_edge(
191
+ edges, seen_edge_keys,
192
+ source=file_nid,
193
+ target=server_nid,
194
+ relation="contains",
195
+ source_file=source_file,
196
+ line=1,
197
+ )
198
+
199
+ command = spec.get("command")
200
+ if isinstance(command, str) and command.strip():
201
+ cmd_label = command.strip()
202
+ cmd_nid = _make_id("mcp_command", cmd_label)
203
+ _add_node(
204
+ nodes, seen_node_ids,
205
+ nid=cmd_nid,
206
+ label=cmd_label,
207
+ kind="mcp_command",
208
+ source_file=source_file,
209
+ line=1,
210
+ )
211
+ _add_edge(
212
+ edges, seen_edge_keys,
213
+ source=server_nid,
214
+ target=cmd_nid,
215
+ relation="references",
216
+ source_file=source_file,
217
+ line=1,
218
+ context="command",
219
+ )
220
+
221
+ args = spec.get("args")
222
+ if isinstance(args, list):
223
+ package = _detect_package_from_args(args)
224
+ if package:
225
+ pkg_nid = _make_id("mcp_package", package)
226
+ _add_node(
227
+ nodes, seen_node_ids,
228
+ nid=pkg_nid,
229
+ label=package,
230
+ kind="mcp_package",
231
+ source_file=source_file,
232
+ line=1,
233
+ )
234
+ _add_edge(
235
+ edges, seen_edge_keys,
236
+ source=server_nid,
237
+ target=pkg_nid,
238
+ relation="references",
239
+ source_file=source_file,
240
+ line=1,
241
+ context="package",
242
+ )
243
+
244
+ env = spec.get("env")
245
+ if isinstance(env, dict):
246
+ # ONLY KEYS. Values may contain secrets and are never read here.
247
+ for env_name in env.keys():
248
+ if not isinstance(env_name, str) or not env_name:
249
+ continue
250
+ env_nid = _make_id("env_var", env_name)
251
+ _add_node(
252
+ nodes, seen_node_ids,
253
+ nid=env_nid,
254
+ label=env_name,
255
+ kind="env_var",
256
+ source_file=source_file,
257
+ line=1,
258
+ )
259
+ _add_edge(
260
+ edges, seen_edge_keys,
261
+ source=server_nid,
262
+ target=env_nid,
263
+ relation="requires_env",
264
+ source_file=source_file,
265
+ line=1,
266
+ )
267
+
268
+
269
+ # ── Package detection from args ───────────────────────────────────────────────
270
+
271
+ # Patterns observed in real MCP server configs:
272
+ # ["-y", "@modelcontextprotocol/server-filesystem", "/data"] (npx)
273
+ # ["-y", "@org/pkg@1.2.3"]
274
+ # ["mcp-server-fetch"] (uvx / python)
275
+ # ["mcp-server-time", "--local-timezone=UTC"]
276
+ # ["@scoped/some-mcp"] (pnpx)
277
+ # ["mcp-server-fetch"] (uvx direct)
278
+ _NPM_PKG_RE = re.compile(r"^@[a-z0-9][a-z0-9._-]*/[a-z0-9][a-z0-9._-]*(?:@[\w.\-+]+)?$")
279
+ _PY_MCP_PKG_RE = re.compile(r"^[a-z0-9][a-z0-9._-]*-mcp(?:-[a-z0-9._-]+)?$|^mcp-[a-z0-9][a-z0-9._-]*$")
280
+ _ARG_FLAG_RE = re.compile(r"^-{1,2}\w")
281
+
282
+
283
+ def _detect_package_from_args(args: list[Any]) -> str | None:
284
+ """Return the first arg that looks like an npm or pypi package id, else None.
285
+
286
+ Skips short flags (-y, --yes) and option arguments (--local-timezone=UTC).
287
+ """
288
+ for raw in args:
289
+ if not isinstance(raw, str):
290
+ continue
291
+ arg = raw.strip()
292
+ if not arg or _ARG_FLAG_RE.match(arg):
293
+ continue
294
+ if _NPM_PKG_RE.match(arg):
295
+ return _strip_version(arg)
296
+ if _PY_MCP_PKG_RE.match(arg):
297
+ return arg
298
+ return None
299
+
300
+
301
+ def _strip_version(pkg: str) -> str:
302
+ """Drop the ``@version`` suffix from an npm package id, preserving the scope.
303
+
304
+ Scoped: ``@scope/name`` or ``@scope/name@1.2.3`` — there are at most two
305
+ ``@`` chars; the second is the version separator.
306
+ Unscoped: ``name`` or ``name@1.2.3``.
307
+ """
308
+ if pkg.startswith("@"):
309
+ version_at = pkg.find("@", 1)
310
+ return pkg if version_at == -1 else pkg[:version_at]
311
+ version_at = pkg.find("@")
312
+ return pkg if version_at == -1 else pkg[:version_at]
313
+
314
+
315
+ # ── Node / edge construction (Graphify schema) ────────────────────────────────
316
+
317
+
318
+ def _add_node(
319
+ nodes: list[dict[str, Any]],
320
+ seen: set[str],
321
+ *,
322
+ nid: str,
323
+ label: str,
324
+ kind: str,
325
+ source_file: str,
326
+ line: int,
327
+ ) -> None:
328
+ """Append a node if not already present. ``kind`` is metadata, not file_type."""
329
+ if not nid or nid in seen:
330
+ return
331
+ seen.add(nid)
332
+ nodes.append({
333
+ "id": nid,
334
+ "label": sanitize_label(label),
335
+ "file_type": "code",
336
+ "source_file": source_file,
337
+ "source_location": f"L{line}",
338
+ "metadata": {"mcp_kind": kind},
339
+ })
340
+
341
+
342
+ def _add_edge(
343
+ edges: list[dict[str, Any]],
344
+ seen: set[tuple[str, str, str]],
345
+ *,
346
+ source: str,
347
+ target: str,
348
+ relation: str,
349
+ source_file: str,
350
+ line: int,
351
+ context: str | None = None,
352
+ ) -> None:
353
+ """Append an edge if (source, target, relation) is not already present."""
354
+ if not source or not target or source == target:
355
+ return
356
+ key = (source, target, relation)
357
+ if key in seen:
358
+ return
359
+ seen.add(key)
360
+ edge: dict[str, Any] = {
361
+ "source": source,
362
+ "target": target,
363
+ "relation": relation,
364
+ "confidence": "EXTRACTED",
365
+ "confidence_score": 1.0,
366
+ "source_file": source_file,
367
+ "source_location": f"L{line}",
368
+ "weight": 1.0,
369
+ }
370
+ if context:
371
+ edge["context"] = context
372
+ edges.append(edge)
373
+
374
+
375
+ # ── ID helpers (kept local; mirror extract.py shape) ──────────────────────────
376
+
377
+
378
+ def _make_id(*parts: str) -> str:
379
+ """Build a stable node ID. Must match extract._make_id's normalisation rules."""
380
+ combined = "_".join(p.strip("_.") for p in parts if p)
381
+ combined = unicodedata.normalize("NFKC", combined)
382
+ cleaned = re.sub(r"[^\w]+", "_", combined, flags=re.UNICODE)
383
+ cleaned = re.sub(r"_+", "_", cleaned)
384
+ return cleaned.strip("_").casefold()
385
+
386
+
387
+ def _file_stem(path: Path) -> str:
388
+ """Mirror extract._file_stem: include parent dir name to disambiguate."""
389
+ parent = path.parent.name
390
+ if parent and parent not in (".", ""):
391
+ return f"{parent}.{path.stem}"
392
+ return path.stem
@@ -0,0 +1,212 @@
1
+ """Runtime compatibility probe for Graphify MultiDiGraph mode.
2
+
3
+ Verifies that the current NetworkX runtime supports the behaviors a future
4
+ opt-in --multigraph build will rely on. The probe is BEHAVIOR-based, not
5
+ version-based — both NX 3.4.2 (Py 3.10 lane) and NX 3.6.1+ (Py 3.11+ lane)
6
+ pass. The probe result is cached for the process lifetime via lru_cache.
7
+
8
+ No call sites added yet; downstream multigraph PRs will gate on
9
+ require_multigraph_capabilities() before enabling MDG mode.
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ from collections.abc import Callable
15
+ from dataclasses import dataclass
16
+ from functools import lru_cache
17
+ import sys
18
+ from typing import Any
19
+
20
+ import networkx as nx
21
+ from networkx.readwrite import json_graph
22
+
23
+
24
+ @dataclass(frozen=True)
25
+ class CapabilityCheck:
26
+ name: str
27
+ ok: bool
28
+ detail: str
29
+
30
+
31
+ @dataclass(frozen=True)
32
+ class MultigraphCapabilityResult:
33
+ python_version: str
34
+ networkx_version: str
35
+ checks: tuple[CapabilityCheck, ...]
36
+
37
+ @property
38
+ def ok(self) -> bool:
39
+ return all(check.ok for check in self.checks)
40
+
41
+ @property
42
+ def failed(self) -> tuple[CapabilityCheck, ...]:
43
+ return tuple(check for check in self.checks if not check.ok)
44
+
45
+ def error_message(self) -> str:
46
+ if self.ok:
47
+ return (
48
+ "Graphify MultiDiGraph capability probe passed "
49
+ f"(Python {self.python_version}, NetworkX {self.networkx_version})."
50
+ )
51
+ failed = "; ".join(f"{check.name}: {check.detail}" for check in self.failed)
52
+ return (
53
+ "error: --multigraph requires NetworkX keyed MultiDiGraph node-link "
54
+ "round-trip support. "
55
+ f"Detected Python {self.python_version}, NetworkX {self.networkx_version}. "
56
+ f"Failed capability check(s): {failed}. "
57
+ "Default simple graph mode remains available."
58
+ )
59
+
60
+
61
+ def _check(name: str, func: Callable[[], bool | str]) -> CapabilityCheck:
62
+ try:
63
+ detail = func()
64
+ except Exception as exc:
65
+ return CapabilityCheck(name, False, f"{type(exc).__name__}: {exc}")
66
+ if detail is True:
67
+ return CapabilityCheck(name, True, "ok")
68
+ if isinstance(detail, str):
69
+ return CapabilityCheck(name, False, detail)
70
+ return CapabilityCheck(name, False, f"unexpected result {detail!r}")
71
+
72
+
73
+ def _build_probe_graph() -> nx.MultiDiGraph:
74
+ graph = nx.MultiDiGraph()
75
+ graph.add_node("a", label="A")
76
+ graph.add_node("b", label="B")
77
+ graph.add_edge("a", "b", key="calls:a.py:L1", relation="calls", source_file="a.py")
78
+ graph.add_edge("a", "b", key="imports:a.py:L2", relation="imports", source_file="a.py")
79
+ return graph
80
+
81
+
82
+ def _probe_keyed_parallel_edges() -> bool | str:
83
+ graph = _build_probe_graph()
84
+ if not graph.is_multigraph() or not graph.is_directed():
85
+ return f"probe graph type was {type(graph).__name__}"
86
+ if graph.number_of_edges("a", "b") != 2:
87
+ return f"expected 2 keyed parallel edges, got {graph.number_of_edges('a', 'b')}"
88
+ keys = set(graph["a"]["b"].keys())
89
+ expected = {"calls:a.py:L1", "imports:a.py:L2"}
90
+ if keys != expected:
91
+ return f"expected keys {sorted(expected)}, got {sorted(keys)}"
92
+ return True
93
+
94
+
95
+ def _probe_node_link_round_trip() -> bool | str:
96
+ graph = _build_probe_graph()
97
+ data = json_graph.node_link_data(graph, edges="links")
98
+ if data.get("multigraph") is not True:
99
+ return f"serialized multigraph flag was {data.get('multigraph')!r}"
100
+ if data.get("directed") is not True:
101
+ return f"serialized directed flag was {data.get('directed')!r}"
102
+ links = data.get("links")
103
+ if not isinstance(links, list) or len(links) != 2:
104
+ length = 0 if not isinstance(links, list) else len(links)
105
+ return f"serialized links length was {length}"
106
+ serialized_keys: set[str] = set()
107
+ for edge in links:
108
+ if isinstance(edge, dict):
109
+ edge_key = edge.get("key")
110
+ if isinstance(edge_key, str):
111
+ serialized_keys.add(edge_key)
112
+ expected = {"calls:a.py:L1", "imports:a.py:L2"}
113
+ if serialized_keys != expected:
114
+ return f"serialized keys {sorted(serialized_keys)} did not match {sorted(expected)}"
115
+ loaded = json_graph.node_link_graph(data, edges="links")
116
+ if not isinstance(loaded, nx.MultiDiGraph):
117
+ return f"round-trip graph type was {type(loaded).__name__}"
118
+ if loaded.number_of_edges("a", "b") != 2:
119
+ return f"round-trip edge count was {loaded.number_of_edges('a', 'b')}"
120
+ loaded_keys = set(loaded["a"]["b"].keys())
121
+ if loaded_keys != expected:
122
+ return f"round-trip keys {sorted(loaded_keys)} did not match {sorted(expected)}"
123
+ return True
124
+
125
+
126
+ def _probe_duplicate_key_overwrite_semantics() -> bool | str:
127
+ graph = nx.MultiDiGraph()
128
+ graph.add_edge("x", "y", key="same", marker="first")
129
+ graph.add_edge("x", "y", key="same", marker="second")
130
+ edges = list(graph.edges(keys=True, data=True))
131
+ if len(edges) != 1:
132
+ return f"expected one edge after duplicate-key add, got {len(edges)}"
133
+ if edges[0][3].get("marker") != "second":
134
+ return f"expected second attr overwrite, got {edges[0][3].get('marker')!r}"
135
+ return True
136
+
137
+
138
+ def _probe_reserved_key_attr_rejected() -> bool | str:
139
+ """Verify the Python language guarantee that NetworkX add_edge inherits.
140
+
141
+ Python forbids passing the same keyword argument twice — once explicitly
142
+ and once via **kwargs. This probe confirms that protection still applies
143
+ to nx.MultiDiGraph.add_edge: a future loader that builds attrs from JSON
144
+ will be reliably protected from accidentally setting `key` via attrs while
145
+ also passing `key=` explicitly.
146
+
147
+ The probe always passes on any Python 3.x version. Its purpose is to
148
+ document the invariant explicitly in the probe suite so that if a future
149
+ Python version relaxes this rule (extremely unlikely), the probe surfaces
150
+ the regression.
151
+ """
152
+ graph = nx.MultiDiGraph()
153
+ attrs: dict[str, Any] = {"key": "attr-key", "relation": "calls"}
154
+ try:
155
+ graph.add_edge("a", "b", key="schema-key", **attrs)
156
+ except TypeError:
157
+ return True
158
+ return "add_edge accepted duplicate key keyword and attr; loader must not rely on this"
159
+
160
+
161
+ def _probe_remove_edges_from_two_tuple_semantics() -> bool | str:
162
+ graph = nx.MultiDiGraph()
163
+ graph.add_edge("a", "b", key="one")
164
+ graph.add_edge("a", "b", key="two")
165
+ graph.remove_edges_from([("a", "b")])
166
+ remaining = graph.number_of_edges("a", "b")
167
+ if remaining != 1:
168
+ return f"expected one remaining edge after two-tuple removal, got {remaining}"
169
+ return True
170
+
171
+
172
+ def _probe_to_undirected_preserves_multigraph_type() -> bool | str:
173
+ graph = _build_probe_graph()
174
+ undirected = graph.to_undirected()
175
+ undirected_view = graph.to_undirected(as_view=True)
176
+ if not isinstance(undirected, nx.MultiGraph):
177
+ return f"to_undirected() returned {type(undirected).__name__}"
178
+ if not isinstance(undirected_view, nx.MultiGraph):
179
+ return f"to_undirected(as_view=True) returned {type(undirected_view).__name__}"
180
+ return True
181
+
182
+
183
+ @lru_cache(maxsize=1)
184
+ def probe_multigraph_capabilities() -> MultigraphCapabilityResult:
185
+ checks = (
186
+ _check("keyed_parallel_edges", _probe_keyed_parallel_edges),
187
+ _check("node_link_edges_links_round_trip", _probe_node_link_round_trip),
188
+ _check("duplicate_key_overwrite_semantics", _probe_duplicate_key_overwrite_semantics),
189
+ _check("reserved_key_attr_rejected", _probe_reserved_key_attr_rejected),
190
+ _check(
191
+ "remove_edges_from_two_tuple_semantics",
192
+ _probe_remove_edges_from_two_tuple_semantics,
193
+ ),
194
+ _check(
195
+ "to_undirected_preserves_multigraph_type",
196
+ _probe_to_undirected_preserves_multigraph_type,
197
+ ),
198
+ )
199
+ return MultigraphCapabilityResult(
200
+ python_version=(
201
+ f"{sys.version_info.major}.{sys.version_info.minor}.{sys.version_info.micro}"
202
+ ),
203
+ networkx_version=nx.__version__,
204
+ checks=checks,
205
+ )
206
+
207
+
208
+ def require_multigraph_capabilities() -> MultigraphCapabilityResult:
209
+ result = probe_multigraph_capabilities()
210
+ if not result.ok:
211
+ raise RuntimeError(result.error_message())
212
+ return result