@oriro/orirocli 0.1.8 → 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 (350) hide show
  1. package/ATTRIBUTION.md +8 -0
  2. package/LICENSE +21 -0
  3. package/package.json +1 -1
  4. package/skills/21stdev/SKILL.md +64 -0
  5. package/skills/graphify/SKILL.md +619 -0
  6. package/skills/graphify/__init__.py +28 -0
  7. package/skills/graphify/__main__.py +4582 -0
  8. package/skills/graphify/affected.py +154 -0
  9. package/skills/graphify/always_on/agents-md.md +12 -0
  10. package/skills/graphify/always_on/antigravity-rules.md +14 -0
  11. package/skills/graphify/always_on/claude-md.md +9 -0
  12. package/skills/graphify/always_on/gemini-md.md +9 -0
  13. package/skills/graphify/always_on/kiro-steering.md +5 -0
  14. package/skills/graphify/always_on/vscode-instructions.md +17 -0
  15. package/skills/graphify/analyze.py +724 -0
  16. package/skills/graphify/benchmark.py +155 -0
  17. package/skills/graphify/build.py +487 -0
  18. package/skills/graphify/cache.py +417 -0
  19. package/skills/graphify/callflow_html.py +2020 -0
  20. package/skills/graphify/cluster.py +272 -0
  21. package/skills/graphify/command-kilo.md +15 -0
  22. package/skills/graphify/dedup.py +429 -0
  23. package/skills/graphify/detect.py +1379 -0
  24. package/skills/graphify/diagnostics.py +390 -0
  25. package/skills/graphify/export.py +1408 -0
  26. package/skills/graphify/extract.py +11570 -0
  27. package/skills/graphify/global_graph.py +159 -0
  28. package/skills/graphify/google_workspace.py +223 -0
  29. package/skills/graphify/hooks.py +457 -0
  30. package/skills/graphify/ingest.py +331 -0
  31. package/skills/graphify/llm.py +1896 -0
  32. package/skills/graphify/manifest.py +4 -0
  33. package/skills/graphify/mcp_ingest.py +392 -0
  34. package/skills/graphify/multigraph_compat.py +212 -0
  35. package/skills/graphify/pg_introspect.py +142 -0
  36. package/skills/graphify/prs.py +748 -0
  37. package/skills/graphify/querylog.py +70 -0
  38. package/skills/graphify/report.py +218 -0
  39. package/skills/graphify/scip_ingest.py +363 -0
  40. package/skills/graphify/security.py +336 -0
  41. package/skills/graphify/semantic_cleanup.py +319 -0
  42. package/skills/graphify/serve.py +1309 -0
  43. package/skills/graphify/skill-aider.md +1246 -0
  44. package/skills/graphify/skill-amp.md +613 -0
  45. package/skills/graphify/skill-claw.md +616 -0
  46. package/skills/graphify/skill-codex.md +613 -0
  47. package/skills/graphify/skill-copilot.md +616 -0
  48. package/skills/graphify/skill-devin.md +1372 -0
  49. package/skills/graphify/skill-droid.md +613 -0
  50. package/skills/graphify/skill-kilo.md +625 -0
  51. package/skills/graphify/skill-kiro.md +615 -0
  52. package/skills/graphify/skill-opencode.md +608 -0
  53. package/skills/graphify/skill-pi.md +615 -0
  54. package/skills/graphify/skill-trae.md +614 -0
  55. package/skills/graphify/skill-vscode.md +612 -0
  56. package/skills/graphify/skill-windows.md +651 -0
  57. package/skills/graphify/skills/amp/references/add-watch.md +56 -0
  58. package/skills/graphify/skills/amp/references/exports.md +71 -0
  59. package/skills/graphify/skills/amp/references/extraction-spec.md +68 -0
  60. package/skills/graphify/skills/amp/references/github-and-merge.md +46 -0
  61. package/skills/graphify/skills/amp/references/hooks.md +33 -0
  62. package/skills/graphify/skills/amp/references/query.md +249 -0
  63. package/skills/graphify/skills/amp/references/transcribe.md +48 -0
  64. package/skills/graphify/skills/amp/references/update.md +179 -0
  65. package/skills/graphify/skills/claude/references/add-watch.md +56 -0
  66. package/skills/graphify/skills/claude/references/exports.md +71 -0
  67. package/skills/graphify/skills/claude/references/extraction-spec.md +68 -0
  68. package/skills/graphify/skills/claude/references/github-and-merge.md +46 -0
  69. package/skills/graphify/skills/claude/references/hooks.md +33 -0
  70. package/skills/graphify/skills/claude/references/query.md +103 -0
  71. package/skills/graphify/skills/claude/references/transcribe.md +48 -0
  72. package/skills/graphify/skills/claude/references/update.md +179 -0
  73. package/skills/graphify/skills/claw/references/add-watch.md +56 -0
  74. package/skills/graphify/skills/claw/references/exports.md +71 -0
  75. package/skills/graphify/skills/claw/references/extraction-spec.md +29 -0
  76. package/skills/graphify/skills/claw/references/github-and-merge.md +46 -0
  77. package/skills/graphify/skills/claw/references/hooks.md +33 -0
  78. package/skills/graphify/skills/claw/references/query.md +249 -0
  79. package/skills/graphify/skills/claw/references/transcribe.md +48 -0
  80. package/skills/graphify/skills/claw/references/update.md +179 -0
  81. package/skills/graphify/skills/codex/references/add-watch.md +56 -0
  82. package/skills/graphify/skills/codex/references/exports.md +71 -0
  83. package/skills/graphify/skills/codex/references/extraction-spec.md +29 -0
  84. package/skills/graphify/skills/codex/references/github-and-merge.md +46 -0
  85. package/skills/graphify/skills/codex/references/hooks.md +33 -0
  86. package/skills/graphify/skills/codex/references/query.md +249 -0
  87. package/skills/graphify/skills/codex/references/transcribe.md +48 -0
  88. package/skills/graphify/skills/codex/references/update.md +179 -0
  89. package/skills/graphify/skills/copilot/references/add-watch.md +56 -0
  90. package/skills/graphify/skills/copilot/references/exports.md +71 -0
  91. package/skills/graphify/skills/copilot/references/extraction-spec.md +68 -0
  92. package/skills/graphify/skills/copilot/references/github-and-merge.md +46 -0
  93. package/skills/graphify/skills/copilot/references/hooks.md +33 -0
  94. package/skills/graphify/skills/copilot/references/query.md +249 -0
  95. package/skills/graphify/skills/copilot/references/transcribe.md +48 -0
  96. package/skills/graphify/skills/copilot/references/update.md +179 -0
  97. package/skills/graphify/skills/droid/references/add-watch.md +56 -0
  98. package/skills/graphify/skills/droid/references/exports.md +71 -0
  99. package/skills/graphify/skills/droid/references/extraction-spec.md +68 -0
  100. package/skills/graphify/skills/droid/references/github-and-merge.md +46 -0
  101. package/skills/graphify/skills/droid/references/hooks.md +33 -0
  102. package/skills/graphify/skills/droid/references/query.md +249 -0
  103. package/skills/graphify/skills/droid/references/transcribe.md +48 -0
  104. package/skills/graphify/skills/droid/references/update.md +179 -0
  105. package/skills/graphify/skills/kilo/references/add-watch.md +56 -0
  106. package/skills/graphify/skills/kilo/references/exports.md +71 -0
  107. package/skills/graphify/skills/kilo/references/extraction-spec.md +68 -0
  108. package/skills/graphify/skills/kilo/references/github-and-merge.md +46 -0
  109. package/skills/graphify/skills/kilo/references/hooks.md +33 -0
  110. package/skills/graphify/skills/kilo/references/query.md +249 -0
  111. package/skills/graphify/skills/kilo/references/transcribe.md +48 -0
  112. package/skills/graphify/skills/kilo/references/update.md +179 -0
  113. package/skills/graphify/skills/kiro/references/add-watch.md +56 -0
  114. package/skills/graphify/skills/kiro/references/exports.md +71 -0
  115. package/skills/graphify/skills/kiro/references/extraction-spec.md +29 -0
  116. package/skills/graphify/skills/kiro/references/github-and-merge.md +46 -0
  117. package/skills/graphify/skills/kiro/references/hooks.md +33 -0
  118. package/skills/graphify/skills/kiro/references/query.md +249 -0
  119. package/skills/graphify/skills/kiro/references/transcribe.md +48 -0
  120. package/skills/graphify/skills/kiro/references/update.md +179 -0
  121. package/skills/graphify/skills/opencode/references/add-watch.md +56 -0
  122. package/skills/graphify/skills/opencode/references/exports.md +71 -0
  123. package/skills/graphify/skills/opencode/references/extraction-spec.md +68 -0
  124. package/skills/graphify/skills/opencode/references/github-and-merge.md +46 -0
  125. package/skills/graphify/skills/opencode/references/hooks.md +33 -0
  126. package/skills/graphify/skills/opencode/references/query.md +249 -0
  127. package/skills/graphify/skills/opencode/references/transcribe.md +48 -0
  128. package/skills/graphify/skills/opencode/references/update.md +179 -0
  129. package/skills/graphify/skills/pi/references/add-watch.md +56 -0
  130. package/skills/graphify/skills/pi/references/exports.md +71 -0
  131. package/skills/graphify/skills/pi/references/extraction-spec.md +29 -0
  132. package/skills/graphify/skills/pi/references/github-and-merge.md +46 -0
  133. package/skills/graphify/skills/pi/references/hooks.md +33 -0
  134. package/skills/graphify/skills/pi/references/query.md +249 -0
  135. package/skills/graphify/skills/pi/references/transcribe.md +48 -0
  136. package/skills/graphify/skills/pi/references/update.md +179 -0
  137. package/skills/graphify/skills/trae/references/add-watch.md +56 -0
  138. package/skills/graphify/skills/trae/references/exports.md +71 -0
  139. package/skills/graphify/skills/trae/references/extraction-spec.md +68 -0
  140. package/skills/graphify/skills/trae/references/github-and-merge.md +46 -0
  141. package/skills/graphify/skills/trae/references/hooks.md +35 -0
  142. package/skills/graphify/skills/trae/references/query.md +249 -0
  143. package/skills/graphify/skills/trae/references/transcribe.md +48 -0
  144. package/skills/graphify/skills/trae/references/update.md +179 -0
  145. package/skills/graphify/skills/vscode/references/add-watch.md +56 -0
  146. package/skills/graphify/skills/vscode/references/exports.md +71 -0
  147. package/skills/graphify/skills/vscode/references/extraction-spec.md +68 -0
  148. package/skills/graphify/skills/vscode/references/github-and-merge.md +46 -0
  149. package/skills/graphify/skills/vscode/references/hooks.md +33 -0
  150. package/skills/graphify/skills/vscode/references/query.md +249 -0
  151. package/skills/graphify/skills/vscode/references/transcribe.md +48 -0
  152. package/skills/graphify/skills/vscode/references/update.md +179 -0
  153. package/skills/graphify/skills/windows/references/add-watch.md +56 -0
  154. package/skills/graphify/skills/windows/references/exports.md +71 -0
  155. package/skills/graphify/skills/windows/references/extraction-spec.md +68 -0
  156. package/skills/graphify/skills/windows/references/github-and-merge.md +46 -0
  157. package/skills/graphify/skills/windows/references/hooks.md +33 -0
  158. package/skills/graphify/skills/windows/references/query.md +249 -0
  159. package/skills/graphify/skills/windows/references/transcribe.md +48 -0
  160. package/skills/graphify/skills/windows/references/update.md +179 -0
  161. package/skills/graphify/symbol_resolution.py +538 -0
  162. package/skills/graphify/transcribe.py +184 -0
  163. package/skills/graphify/tree_html.py +582 -0
  164. package/skills/graphify/validate.py +72 -0
  165. package/skills/graphify/watch.py +898 -0
  166. package/skills/graphify/wiki.py +282 -0
  167. package/skills/impeccable/SKILL.md +186 -0
  168. package/skills/impeccable/agents/impeccable_asset_producer.toml +92 -0
  169. package/skills/impeccable/agents/impeccable_manual_edit_applier.toml +95 -0
  170. package/skills/impeccable/agents/openai.yaml +4 -0
  171. package/skills/impeccable/reference/adapt.md +311 -0
  172. package/skills/impeccable/reference/animate.md +201 -0
  173. package/skills/impeccable/reference/audit.md +133 -0
  174. package/skills/impeccable/reference/bolder.md +113 -0
  175. package/skills/impeccable/reference/brand.md +108 -0
  176. package/skills/impeccable/reference/clarify.md +288 -0
  177. package/skills/impeccable/reference/codex.md +105 -0
  178. package/skills/impeccable/reference/colorize.md +257 -0
  179. package/skills/impeccable/reference/craft.md +123 -0
  180. package/skills/impeccable/reference/critique.md +790 -0
  181. package/skills/impeccable/reference/delight.md +302 -0
  182. package/skills/impeccable/reference/distill.md +111 -0
  183. package/skills/impeccable/reference/document.md +429 -0
  184. package/skills/impeccable/reference/extract.md +69 -0
  185. package/skills/impeccable/reference/harden.md +347 -0
  186. package/skills/impeccable/reference/init.md +172 -0
  187. package/skills/impeccable/reference/interaction-design.md +189 -0
  188. package/skills/impeccable/reference/layout.md +161 -0
  189. package/skills/impeccable/reference/live.md +720 -0
  190. package/skills/impeccable/reference/onboard.md +234 -0
  191. package/skills/impeccable/reference/optimize.md +258 -0
  192. package/skills/impeccable/reference/overdrive.md +130 -0
  193. package/skills/impeccable/reference/polish.md +241 -0
  194. package/skills/impeccable/reference/product.md +60 -0
  195. package/skills/impeccable/reference/quieter.md +99 -0
  196. package/skills/impeccable/reference/shape.md +165 -0
  197. package/skills/impeccable/reference/typeset.md +279 -0
  198. package/skills/impeccable/scripts/cleanup-deprecated.mjs +284 -0
  199. package/skills/impeccable/scripts/command-metadata.json +94 -0
  200. package/skills/impeccable/scripts/context-signals.mjs +225 -0
  201. package/skills/impeccable/scripts/context.mjs +266 -0
  202. package/skills/impeccable/scripts/critique-storage.mjs +242 -0
  203. package/skills/impeccable/scripts/design-parser.mjs +835 -0
  204. package/skills/impeccable/scripts/detect-csp.mjs +198 -0
  205. package/skills/impeccable/scripts/detect.mjs +21 -0
  206. package/skills/impeccable/scripts/detector/browser/injected/index.mjs +1733 -0
  207. package/skills/impeccable/scripts/detector/cli/main.mjs +244 -0
  208. package/skills/impeccable/scripts/detector/detect-antipatterns-browser.js +4618 -0
  209. package/skills/impeccable/scripts/detector/detect-antipatterns.mjs +43 -0
  210. package/skills/impeccable/scripts/detector/engines/browser/detect-url.mjs +252 -0
  211. package/skills/impeccable/scripts/detector/engines/regex/detect-text.mjs +535 -0
  212. package/skills/impeccable/scripts/detector/engines/static-html/css-cascade.mjs +986 -0
  213. package/skills/impeccable/scripts/detector/engines/static-html/detect-html.mjs +208 -0
  214. package/skills/impeccable/scripts/detector/engines/visual/screenshot-contrast.mjs +189 -0
  215. package/skills/impeccable/scripts/detector/findings.mjs +12 -0
  216. package/skills/impeccable/scripts/detector/node/file-system.mjs +198 -0
  217. package/skills/impeccable/scripts/detector/profile/profiler.mjs +166 -0
  218. package/skills/impeccable/scripts/detector/registry/antipatterns.mjs +419 -0
  219. package/skills/impeccable/scripts/detector/rules/checks.mjs +2384 -0
  220. package/skills/impeccable/scripts/detector/shared/color.mjs +124 -0
  221. package/skills/impeccable/scripts/detector/shared/constants.mjs +101 -0
  222. package/skills/impeccable/scripts/detector/shared/page.mjs +7 -0
  223. package/skills/impeccable/scripts/impeccable-paths.mjs +126 -0
  224. package/skills/impeccable/scripts/is-generated.mjs +69 -0
  225. package/skills/impeccable/scripts/live-accept.mjs +812 -0
  226. package/skills/impeccable/scripts/live-browser-session.js +123 -0
  227. package/skills/impeccable/scripts/live-browser.js +10295 -0
  228. package/skills/impeccable/scripts/live-commit-manual-edits.mjs +1241 -0
  229. package/skills/impeccable/scripts/live-complete.mjs +75 -0
  230. package/skills/impeccable/scripts/live-completion.mjs +19 -0
  231. package/skills/impeccable/scripts/live-copy-edit-agent.mjs +683 -0
  232. package/skills/impeccable/scripts/live-discard-manual-edits.mjs +51 -0
  233. package/skills/impeccable/scripts/live-event-validation.mjs +137 -0
  234. package/skills/impeccable/scripts/live-inject.mjs +557 -0
  235. package/skills/impeccable/scripts/live-insert-ui.mjs +458 -0
  236. package/skills/impeccable/scripts/live-insert.mjs +272 -0
  237. package/skills/impeccable/scripts/live-manual-edit-evidence.mjs +363 -0
  238. package/skills/impeccable/scripts/live-manual-edits-buffer.mjs +152 -0
  239. package/skills/impeccable/scripts/live-poll.mjs +379 -0
  240. package/skills/impeccable/scripts/live-resume.mjs +94 -0
  241. package/skills/impeccable/scripts/live-server.mjs +2326 -0
  242. package/skills/impeccable/scripts/live-session-store.mjs +289 -0
  243. package/skills/impeccable/scripts/live-status.mjs +61 -0
  244. package/skills/impeccable/scripts/live-svelte-component.mjs +826 -0
  245. package/skills/impeccable/scripts/live-sveltekit-adapter.mjs +274 -0
  246. package/skills/impeccable/scripts/live-ui-core.mjs +179 -0
  247. package/skills/impeccable/scripts/live-vocabulary.mjs +36 -0
  248. package/skills/impeccable/scripts/live-wrap.mjs +894 -0
  249. package/skills/impeccable/scripts/live.mjs +246 -0
  250. package/skills/impeccable/scripts/modern-screenshot.umd.js +14 -0
  251. package/skills/impeccable/scripts/palette.mjs +633 -0
  252. package/skills/impeccable/scripts/pin.mjs +214 -0
  253. package/skills/uipm-ui-styling/LICENSE.txt +202 -0
  254. package/skills/uipm-ui-styling/SKILL.md +328 -0
  255. package/skills/uipm-ui-styling/canvas-fonts/ArsenalSC-OFL.txt +93 -0
  256. package/skills/uipm-ui-styling/canvas-fonts/ArsenalSC-Regular.ttf +0 -0
  257. package/skills/uipm-ui-styling/canvas-fonts/BigShoulders-Bold.ttf +0 -0
  258. package/skills/uipm-ui-styling/canvas-fonts/BigShoulders-OFL.txt +93 -0
  259. package/skills/uipm-ui-styling/canvas-fonts/BigShoulders-Regular.ttf +0 -0
  260. package/skills/uipm-ui-styling/canvas-fonts/Boldonse-OFL.txt +93 -0
  261. package/skills/uipm-ui-styling/canvas-fonts/Boldonse-Regular.ttf +0 -0
  262. package/skills/uipm-ui-styling/canvas-fonts/BricolageGrotesque-Bold.ttf +0 -0
  263. package/skills/uipm-ui-styling/canvas-fonts/BricolageGrotesque-OFL.txt +93 -0
  264. package/skills/uipm-ui-styling/canvas-fonts/BricolageGrotesque-Regular.ttf +0 -0
  265. package/skills/uipm-ui-styling/canvas-fonts/CrimsonPro-Bold.ttf +0 -0
  266. package/skills/uipm-ui-styling/canvas-fonts/CrimsonPro-Italic.ttf +0 -0
  267. package/skills/uipm-ui-styling/canvas-fonts/CrimsonPro-OFL.txt +93 -0
  268. package/skills/uipm-ui-styling/canvas-fonts/CrimsonPro-Regular.ttf +0 -0
  269. package/skills/uipm-ui-styling/canvas-fonts/DMMono-OFL.txt +93 -0
  270. package/skills/uipm-ui-styling/canvas-fonts/DMMono-Regular.ttf +0 -0
  271. package/skills/uipm-ui-styling/canvas-fonts/EricaOne-OFL.txt +94 -0
  272. package/skills/uipm-ui-styling/canvas-fonts/EricaOne-Regular.ttf +0 -0
  273. package/skills/uipm-ui-styling/canvas-fonts/GeistMono-Bold.ttf +0 -0
  274. package/skills/uipm-ui-styling/canvas-fonts/GeistMono-OFL.txt +93 -0
  275. package/skills/uipm-ui-styling/canvas-fonts/GeistMono-Regular.ttf +0 -0
  276. package/skills/uipm-ui-styling/canvas-fonts/Gloock-OFL.txt +93 -0
  277. package/skills/uipm-ui-styling/canvas-fonts/Gloock-Regular.ttf +0 -0
  278. package/skills/uipm-ui-styling/canvas-fonts/IBMPlexMono-Bold.ttf +0 -0
  279. package/skills/uipm-ui-styling/canvas-fonts/IBMPlexMono-OFL.txt +93 -0
  280. package/skills/uipm-ui-styling/canvas-fonts/IBMPlexMono-Regular.ttf +0 -0
  281. package/skills/uipm-ui-styling/canvas-fonts/IBMPlexSerif-Bold.ttf +0 -0
  282. package/skills/uipm-ui-styling/canvas-fonts/IBMPlexSerif-BoldItalic.ttf +0 -0
  283. package/skills/uipm-ui-styling/canvas-fonts/IBMPlexSerif-Italic.ttf +0 -0
  284. package/skills/uipm-ui-styling/canvas-fonts/IBMPlexSerif-Regular.ttf +0 -0
  285. package/skills/uipm-ui-styling/canvas-fonts/InstrumentSans-Bold.ttf +0 -0
  286. package/skills/uipm-ui-styling/canvas-fonts/InstrumentSans-BoldItalic.ttf +0 -0
  287. package/skills/uipm-ui-styling/canvas-fonts/InstrumentSans-Italic.ttf +0 -0
  288. package/skills/uipm-ui-styling/canvas-fonts/InstrumentSans-OFL.txt +93 -0
  289. package/skills/uipm-ui-styling/canvas-fonts/InstrumentSans-Regular.ttf +0 -0
  290. package/skills/uipm-ui-styling/canvas-fonts/InstrumentSerif-Italic.ttf +0 -0
  291. package/skills/uipm-ui-styling/canvas-fonts/InstrumentSerif-Regular.ttf +0 -0
  292. package/skills/uipm-ui-styling/canvas-fonts/Italiana-OFL.txt +93 -0
  293. package/skills/uipm-ui-styling/canvas-fonts/Italiana-Regular.ttf +0 -0
  294. package/skills/uipm-ui-styling/canvas-fonts/JetBrainsMono-Bold.ttf +0 -0
  295. package/skills/uipm-ui-styling/canvas-fonts/JetBrainsMono-OFL.txt +93 -0
  296. package/skills/uipm-ui-styling/canvas-fonts/JetBrainsMono-Regular.ttf +0 -0
  297. package/skills/uipm-ui-styling/canvas-fonts/Jura-Light.ttf +0 -0
  298. package/skills/uipm-ui-styling/canvas-fonts/Jura-Medium.ttf +0 -0
  299. package/skills/uipm-ui-styling/canvas-fonts/Jura-OFL.txt +93 -0
  300. package/skills/uipm-ui-styling/canvas-fonts/LibreBaskerville-OFL.txt +93 -0
  301. package/skills/uipm-ui-styling/canvas-fonts/LibreBaskerville-Regular.ttf +0 -0
  302. package/skills/uipm-ui-styling/canvas-fonts/Lora-Bold.ttf +0 -0
  303. package/skills/uipm-ui-styling/canvas-fonts/Lora-BoldItalic.ttf +0 -0
  304. package/skills/uipm-ui-styling/canvas-fonts/Lora-Italic.ttf +0 -0
  305. package/skills/uipm-ui-styling/canvas-fonts/Lora-OFL.txt +93 -0
  306. package/skills/uipm-ui-styling/canvas-fonts/Lora-Regular.ttf +0 -0
  307. package/skills/uipm-ui-styling/canvas-fonts/NationalPark-Bold.ttf +0 -0
  308. package/skills/uipm-ui-styling/canvas-fonts/NationalPark-OFL.txt +93 -0
  309. package/skills/uipm-ui-styling/canvas-fonts/NationalPark-Regular.ttf +0 -0
  310. package/skills/uipm-ui-styling/canvas-fonts/NothingYouCouldDo-OFL.txt +93 -0
  311. package/skills/uipm-ui-styling/canvas-fonts/NothingYouCouldDo-Regular.ttf +0 -0
  312. package/skills/uipm-ui-styling/canvas-fonts/Outfit-Bold.ttf +0 -0
  313. package/skills/uipm-ui-styling/canvas-fonts/Outfit-OFL.txt +93 -0
  314. package/skills/uipm-ui-styling/canvas-fonts/Outfit-Regular.ttf +0 -0
  315. package/skills/uipm-ui-styling/canvas-fonts/PixelifySans-Medium.ttf +0 -0
  316. package/skills/uipm-ui-styling/canvas-fonts/PixelifySans-OFL.txt +93 -0
  317. package/skills/uipm-ui-styling/canvas-fonts/PoiretOne-OFL.txt +93 -0
  318. package/skills/uipm-ui-styling/canvas-fonts/PoiretOne-Regular.ttf +0 -0
  319. package/skills/uipm-ui-styling/canvas-fonts/RedHatMono-Bold.ttf +0 -0
  320. package/skills/uipm-ui-styling/canvas-fonts/RedHatMono-OFL.txt +93 -0
  321. package/skills/uipm-ui-styling/canvas-fonts/RedHatMono-Regular.ttf +0 -0
  322. package/skills/uipm-ui-styling/canvas-fonts/Silkscreen-OFL.txt +93 -0
  323. package/skills/uipm-ui-styling/canvas-fonts/Silkscreen-Regular.ttf +0 -0
  324. package/skills/uipm-ui-styling/canvas-fonts/SmoochSans-Medium.ttf +0 -0
  325. package/skills/uipm-ui-styling/canvas-fonts/SmoochSans-OFL.txt +93 -0
  326. package/skills/uipm-ui-styling/canvas-fonts/Tektur-Medium.ttf +0 -0
  327. package/skills/uipm-ui-styling/canvas-fonts/Tektur-OFL.txt +93 -0
  328. package/skills/uipm-ui-styling/canvas-fonts/Tektur-Regular.ttf +0 -0
  329. package/skills/uipm-ui-styling/canvas-fonts/WorkSans-Bold.ttf +0 -0
  330. package/skills/uipm-ui-styling/canvas-fonts/WorkSans-BoldItalic.ttf +0 -0
  331. package/skills/uipm-ui-styling/canvas-fonts/WorkSans-Italic.ttf +0 -0
  332. package/skills/uipm-ui-styling/canvas-fonts/WorkSans-OFL.txt +93 -0
  333. package/skills/uipm-ui-styling/canvas-fonts/WorkSans-Regular.ttf +0 -0
  334. package/skills/uipm-ui-styling/canvas-fonts/YoungSerif-OFL.txt +93 -0
  335. package/skills/uipm-ui-styling/canvas-fonts/YoungSerif-Regular.ttf +0 -0
  336. package/skills/uipm-ui-styling/references/canvas-design-system.md +320 -0
  337. package/skills/uipm-ui-styling/references/shadcn-accessibility.md +471 -0
  338. package/skills/uipm-ui-styling/references/shadcn-components.md +424 -0
  339. package/skills/uipm-ui-styling/references/shadcn-theming.md +373 -0
  340. package/skills/uipm-ui-styling/references/tailwind-customization.md +483 -0
  341. package/skills/uipm-ui-styling/references/tailwind-responsive.md +382 -0
  342. package/skills/uipm-ui-styling/references/tailwind-utilities.md +455 -0
  343. package/skills/uipm-ui-styling/scripts/.coverage +0 -0
  344. package/skills/uipm-ui-styling/scripts/requirements.txt +17 -0
  345. package/skills/uipm-ui-styling/scripts/shadcn_add.py +292 -0
  346. package/skills/uipm-ui-styling/scripts/tailwind_config_gen.py +456 -0
  347. package/skills/uipm-ui-styling/scripts/tests/coverage-ui.json +1 -0
  348. package/skills/uipm-ui-styling/scripts/tests/requirements.txt +3 -0
  349. package/skills/uipm-ui-styling/scripts/tests/test_shadcn_add.py +266 -0
  350. package/skills/uipm-ui-styling/scripts/tests/test_tailwind_config_gen.py +336 -0
@@ -0,0 +1,155 @@
1
+ """Token-reduction benchmark - measures how much context graphify saves vs naive full-corpus approach."""
2
+ from __future__ import annotations
3
+ import json
4
+ import sys
5
+ from pathlib import Path
6
+ import networkx as nx
7
+ from networkx.readwrite import json_graph
8
+
9
+ from graphify.build import edge_data
10
+ from graphify.serve import _query_terms
11
+
12
+
13
+ _CHARS_PER_TOKEN = 4 # standard approximation
14
+
15
+
16
+ def _safe(unicode_char: str, ascii_fallback: str) -> str:
17
+ """Return unicode_char if stdout can encode it, else ascii_fallback.
18
+
19
+ Windows consoles often default to cp1252 which cannot encode box-drawing
20
+ or arrow glyphs; printing them raises UnicodeEncodeError mid-output.
21
+ """
22
+ encoding = getattr(sys.stdout, "encoding", None) or ""
23
+ try:
24
+ unicode_char.encode(encoding)
25
+ return unicode_char
26
+ except (UnicodeEncodeError, LookupError):
27
+ return ascii_fallback
28
+
29
+
30
+ def _hr(width: int = 50) -> str:
31
+ """Horizontal rule that survives non-UTF-8 stdout (e.g. Windows cp1252 console)."""
32
+ return _safe("─", "-") * width
33
+
34
+
35
+ def _estimate_tokens(text: str) -> int:
36
+ return max(1, len(text) // _CHARS_PER_TOKEN)
37
+
38
+
39
+ def _query_subgraph_tokens(G: nx.Graph, question: str, depth: int = 3) -> int:
40
+ """Run BFS from best-matching nodes and return estimated tokens in the subgraph context."""
41
+ terms = _query_terms(question)
42
+ scored = []
43
+ for nid, data in G.nodes(data=True):
44
+ label = data.get("label", "").lower()
45
+ score = sum(1 for t in terms if t in label)
46
+ if score > 0:
47
+ scored.append((score, nid))
48
+ scored.sort(reverse=True)
49
+ start_nodes = [nid for _, nid in scored[:3]]
50
+ if not start_nodes:
51
+ return 0
52
+
53
+ visited: set[str] = set(start_nodes)
54
+ frontier = set(start_nodes)
55
+ edges_seen: list[tuple] = []
56
+ for _ in range(depth):
57
+ next_frontier: set[str] = set()
58
+ for n in frontier:
59
+ for neighbor in G.neighbors(n):
60
+ if neighbor not in visited:
61
+ next_frontier.add(neighbor)
62
+ edges_seen.append((n, neighbor))
63
+ visited.update(next_frontier)
64
+ frontier = next_frontier
65
+
66
+ lines = []
67
+ for nid in visited:
68
+ d = G.nodes[nid]
69
+ lines.append(f"NODE {d.get('label', nid)} src={d.get('source_file', '')} loc={d.get('source_location', '')}")
70
+ for u, v in edges_seen:
71
+ if u in visited and v in visited:
72
+ d = edge_data(G, u, v)
73
+ lines.append(f"EDGE {G.nodes[u].get('label', u)} --{d.get('relation', '')}--> {G.nodes[v].get('label', v)}")
74
+
75
+ return _estimate_tokens("\n".join(lines))
76
+
77
+
78
+ _SAMPLE_QUESTIONS = [
79
+ "how does authentication work",
80
+ "what is the main entry point",
81
+ "how are errors handled",
82
+ "what connects the data layer to the api",
83
+ "what are the core abstractions",
84
+ ]
85
+
86
+
87
+ def run_benchmark(
88
+ graph_path: str = "graphify-out/graph.json",
89
+ corpus_words: int | None = None,
90
+ questions: list[str] | None = None,
91
+ ) -> dict:
92
+ """Measure token reduction: corpus tokens vs graphify query tokens.
93
+
94
+ Args:
95
+ graph_path: path to the built graph
96
+ corpus_words: total word count from detect() output; if None, estimated from graph
97
+ questions: list of questions to benchmark; defaults to _SAMPLE_QUESTIONS
98
+
99
+ Returns dict with: corpus_tokens, avg_query_tokens, reduction_ratio, per_question
100
+ """
101
+ from graphify.security import check_graph_file_size_cap
102
+ check_graph_file_size_cap(Path(graph_path))
103
+ data = json.loads(Path(graph_path).read_text(encoding="utf-8"))
104
+ try:
105
+ G = json_graph.node_link_graph(data, edges="links")
106
+ except TypeError:
107
+ G = json_graph.node_link_graph(data)
108
+
109
+ if corpus_words is None:
110
+ # Rough estimate: each node label is ~3 words, plus source context
111
+ corpus_words = G.number_of_nodes() * 50
112
+
113
+ corpus_tokens = corpus_words * 100 // 75 # words → tokens (100 words ≈ 133 tokens)
114
+
115
+ qs = questions or _SAMPLE_QUESTIONS
116
+ per_question = []
117
+ for q in qs:
118
+ qt = _query_subgraph_tokens(G, q)
119
+ if qt > 0:
120
+ per_question.append({"question": q, "query_tokens": qt, "reduction": round(corpus_tokens / qt, 1)})
121
+
122
+ if not per_question:
123
+ return {"error": "No matching nodes found for sample questions. Build the graph first."}
124
+
125
+ avg_query_tokens = sum(p["query_tokens"] for p in per_question) // len(per_question)
126
+ reduction_ratio = round(corpus_tokens / avg_query_tokens, 1) if avg_query_tokens > 0 else 0
127
+
128
+ return {
129
+ "corpus_tokens": corpus_tokens,
130
+ "corpus_words": corpus_words,
131
+ "nodes": G.number_of_nodes(),
132
+ "edges": G.number_of_edges(),
133
+ "avg_query_tokens": avg_query_tokens,
134
+ "reduction_ratio": reduction_ratio,
135
+ "per_question": per_question,
136
+ }
137
+
138
+
139
+ def print_benchmark(result: dict) -> None:
140
+ """Print a human-readable benchmark report."""
141
+ if "error" in result:
142
+ print(f"Benchmark error: {result['error']}")
143
+ return
144
+
145
+ print(f"\ngraphify token reduction benchmark")
146
+ print(_hr(50))
147
+ arrow = _safe("→", "->")
148
+ print(f" Corpus: {result['corpus_words']:,} words {arrow} ~{result['corpus_tokens']:,} tokens (naive)")
149
+ print(f" Graph: {result['nodes']:,} nodes, {result['edges']:,} edges")
150
+ print(f" Avg query cost: ~{result['avg_query_tokens']:,} tokens")
151
+ print(f" Reduction: {result['reduction_ratio']}x fewer tokens per query")
152
+ print(f"\n Per question:")
153
+ for p in result["per_question"]:
154
+ print(f" [{p['reduction']}x] {p['question'][:55]}")
155
+ print()
@@ -0,0 +1,487 @@
1
+ # assemble node+edge dicts into a NetworkX graph, preserving edge direction
2
+ #
3
+ # Node deduplication — three layers:
4
+ #
5
+ # 1. Within a file (AST): each extractor tracks a `seen_ids` set. A node ID is
6
+ # emitted at most once per file, so duplicate class/function definitions in
7
+ # the same source file are collapsed to the first occurrence.
8
+ #
9
+ # 2. Between files (build): NetworkX G.add_node() is idempotent — calling it
10
+ # twice with the same ID overwrites the attributes with the second call's
11
+ # values. Nodes are added in extraction order (AST first, then semantic),
12
+ # so if the same entity is extracted by both passes the semantic node
13
+ # silently overwrites the AST node. This is intentional: semantic nodes
14
+ # carry richer labels and cross-file context, while AST nodes have precise
15
+ # source_location. If you need to change the priority, reorder extractions
16
+ # passed to build().
17
+ #
18
+ # 3. Semantic merge (skill): before calling build(), the skill merges cached
19
+ # and new semantic results using an explicit `seen` set keyed on node["id"],
20
+ # so duplicates across cache hits and new extractions are resolved there
21
+ # before any graph construction happens.
22
+ #
23
+ from __future__ import annotations
24
+ import json
25
+ import os
26
+ import re
27
+ import sys
28
+ import unicodedata
29
+ from pathlib import Path
30
+ import networkx as nx
31
+ from .validate import validate_extraction
32
+
33
+
34
+ # Synonym mapper for known invalid file_type values that LLM subagents commonly
35
+ # emit. Keeps semantic intent close (markdown→document, tool→code) and falls
36
+ # back to "concept" for any other invalid value (see #840).
37
+ _FILE_TYPE_SYNONYMS = {
38
+ "markdown": "document",
39
+ "text": "document",
40
+ "tool": "code",
41
+ "library": "code",
42
+ "pattern": "concept",
43
+ "principle": "concept",
44
+ "constraint": "concept",
45
+ "tech": "concept",
46
+ "technology": "concept",
47
+ "data-source": "concept",
48
+ "data_source": "concept",
49
+ "gotcha": "concept",
50
+ "framework": "concept",
51
+ }
52
+
53
+
54
+ def _normalize_id(s: str) -> str:
55
+ r"""Normalize an ID string the same way extract._make_id does.
56
+
57
+ Used to reconcile edge endpoints when the LLM generates IDs with slightly
58
+ different punctuation or casing than the AST extractor. Must stay in sync
59
+ with extract._make_id — NFKC normalization, \w with re.UNICODE, underscore
60
+ collapse, and casefold must all match (#811).
61
+ """
62
+ s = unicodedata.normalize("NFKC", s)
63
+ cleaned = re.sub(r"[^\w]+", "_", s, flags=re.UNICODE)
64
+ cleaned = re.sub(r"_+", "_", cleaned)
65
+ return cleaned.strip("_").casefold()
66
+
67
+
68
+ def _norm_source_file(p: str | None, root: str | None = None) -> str | None:
69
+ """Normalize path separators and relativize absolute paths.
70
+
71
+ Converts backslashes to forward slashes (Windows compatibility) and, when
72
+ root is provided, strips the absolute prefix from paths produced by semantic
73
+ subagents so source_file is always repo-relative (fixes #932).
74
+ """
75
+ if not p:
76
+ return p
77
+ p = p.replace("\\", "/")
78
+ if root and os.path.isabs(p):
79
+ try:
80
+ p = Path(p).relative_to(root).as_posix()
81
+ except ValueError:
82
+ pass
83
+ return p
84
+
85
+
86
+ def edge_data(G: nx.Graph, u: str, v: str) -> dict:
87
+ """Return one edge attribute dict for (u, v), tolerating MultiGraph.
88
+
89
+ For MultiGraph/MultiDiGraph there can be multiple parallel edges;
90
+ this returns the first one (sufficient for callers that only need
91
+ relation/confidence for rendering). Fixes #796.
92
+ """
93
+ raw = G[u][v]
94
+ if isinstance(G, (nx.MultiGraph, nx.MultiDiGraph)):
95
+ return next(iter(raw.values()), {})
96
+ return raw
97
+
98
+
99
+ def edge_datas(G: nx.Graph, u: str, v: str) -> list[dict]:
100
+ """Return every edge attribute dict for (u, v); always a list."""
101
+ raw = G[u][v]
102
+ if isinstance(G, (nx.MultiGraph, nx.MultiDiGraph)):
103
+ return list(raw.values())
104
+ return [raw]
105
+
106
+
107
+ def build_from_json(extraction: dict, *, directed: bool = False, root: str | Path | None = None) -> nx.Graph:
108
+ """Build a NetworkX graph from an extraction dict.
109
+
110
+ directed=True produces a DiGraph that preserves edge direction (source→target).
111
+ directed=False (default) produces an undirected Graph for backward compatibility.
112
+ root: if given, absolute source_file paths from semantic subagents are made
113
+ relative to root so all nodes share a consistent path key (#932).
114
+ """
115
+ _root = str(Path(root).resolve()) if root else None
116
+ # NetworkX <= 3.1 serialised edges as "links"; remap to "edges" for compatibility.
117
+ if "edges" not in extraction and "links" in extraction:
118
+ extraction = dict(extraction, edges=extraction["links"])
119
+
120
+ # Canonicalize legacy node/edge schema before validation.
121
+ for node in extraction.get("nodes", []):
122
+ if not isinstance(node, dict):
123
+ continue
124
+ if "source" in node and "source_file" not in node:
125
+ # Count edges that reference this node so the warning is actionable (#479)
126
+ node_id = node.get("id", "?")
127
+ affected_edges = sum(
128
+ 1 for e in extraction.get("edges", [])
129
+ if e.get("source") == node_id or e.get("target") == node_id
130
+ )
131
+ print(
132
+ f"[graphify] WARNING: node '{node_id}' uses field 'source' instead of "
133
+ f"'source_file' — {affected_edges} edge(s) may be misrouted. "
134
+ f"Rename the field to 'source_file' to silence this warning.",
135
+ file=sys.stderr,
136
+ )
137
+ node["source_file"] = node.pop("source")
138
+ # Default missing/None file_type to "concept" so legacy graph.json
139
+ # entries (and stub nodes preserved by `_rebuild_code` from older
140
+ # graphify versions that didn't always populate file_type) don't
141
+ # trigger spurious "invalid file_type 'None'" validator warnings (#660).
142
+ if node.get("file_type") in (None, ""):
143
+ node["file_type"] = "concept"
144
+ ft = node.get("file_type", "")
145
+ if ft and ft not in {"code", "document", "paper", "image", "rationale", "concept"}:
146
+ node["file_type"] = _FILE_TYPE_SYNONYMS.get(ft, "concept")
147
+
148
+ errors = validate_extraction(extraction)
149
+ # Dangling edges (stdlib/external imports) are expected - only warn about real schema errors.
150
+ real_errors = [e for e in errors if "does not match any node id" not in e]
151
+ if real_errors:
152
+ print(f"[graphify] Extraction warning ({len(real_errors)} issues): {real_errors[0]}", file=sys.stderr)
153
+ G: nx.Graph = nx.DiGraph() if directed else nx.Graph()
154
+ for node in extraction.get("nodes", []):
155
+ if "source_file" in node:
156
+ node["source_file"] = _norm_source_file(node["source_file"], _root)
157
+ G.add_node(node["id"], **{k: v for k, v in node.items() if k != "id"})
158
+ node_set = set(G.nodes())
159
+
160
+ # #1145: merge semantic ghost-duplicate nodes into AST nodes.
161
+ # When AST and semantic extractors emit different IDs for the same symbol
162
+ # (one has source_location=L<n>, the other has source_location=None), find
163
+ # pairs that share (source_file basename, label) and collapse the semantic
164
+ # copy into the AST copy so edges re-point to a single node.
165
+ # Two passes: first collect all AST (located) nodes, then find ghosts.
166
+ _loc_nodes: dict[tuple[str, str], str] = {} # (basename, label) -> AST node id
167
+ _noloc_nodes: dict[tuple[str, str], str] = {} # (basename, label) -> semantic node id
168
+ for nid in node_set:
169
+ attrs = G.nodes[nid]
170
+ label = str(attrs.get("label", "")).strip()
171
+ sf = str(attrs.get("source_file", ""))
172
+ basename = Path(sf).name if sf else ""
173
+ if not label or not basename:
174
+ continue
175
+ if attrs.get("source_location"):
176
+ _loc_nodes[(basename, label)] = nid
177
+ for nid in node_set:
178
+ attrs = G.nodes[nid]
179
+ label = str(attrs.get("label", "")).strip()
180
+ sf = str(attrs.get("source_file", ""))
181
+ basename = Path(sf).name if sf else ""
182
+ if not label or not basename or attrs.get("source_location"):
183
+ continue
184
+ key = (basename, label)
185
+ if key in _loc_nodes and _loc_nodes[key] != nid:
186
+ _noloc_nodes[key] = nid
187
+ # For every ghost that has an AST counterpart, record a remap.
188
+ _ghost_remap: dict[str, str] = {} # ghost_id -> canonical_id
189
+ for key, sem_id in _noloc_nodes.items():
190
+ ast_id = _loc_nodes.get(key)
191
+ if ast_id is not None:
192
+ _ghost_remap[sem_id] = ast_id
193
+ # Remove ghost nodes from the graph; edges will be re-pointed via norm_to_id.
194
+ for ghost_id in _ghost_remap:
195
+ G.remove_node(ghost_id)
196
+ node_set.discard(ghost_id)
197
+
198
+ # Normalized ID map: lets edges survive when the LLM generates IDs with
199
+ # slightly different casing or punctuation than the AST extractor.
200
+ # e.g. "Session_ValidateToken" maps to "session_validatetoken".
201
+ norm_to_id: dict[str, str] = {_normalize_id(nid): nid for nid in node_set}
202
+ # Also map ghost IDs to their canonical AST replacements.
203
+ for ghost_id, canonical_id in _ghost_remap.items():
204
+ norm_to_id[_normalize_id(ghost_id)] = canonical_id
205
+ norm_to_id[ghost_id] = canonical_id
206
+ # Iterate edges in a deterministic order. The graph is undirected and stores
207
+ # direction in _src/_tgt; when two edges collapse onto the same node pair the
208
+ # last write wins, so an unstable iteration order flips _src/_tgt run-to-run
209
+ # and makes the serialized graph churn. Sorting fixes the last-write outcome.
210
+ for edge in sorted(
211
+ extraction.get("edges", []),
212
+ key=lambda e: (
213
+ str(e.get("source", e.get("from", ""))),
214
+ str(e.get("target", e.get("to", ""))),
215
+ str(e.get("relation", "")),
216
+ ),
217
+ ):
218
+ if "source" not in edge and "from" in edge:
219
+ edge["source"] = edge["from"]
220
+ if "target" not in edge and "to" in edge:
221
+ edge["target"] = edge["to"]
222
+ if "source" not in edge or "target" not in edge:
223
+ continue
224
+ src, tgt = edge["source"], edge["target"]
225
+ # Remap mismatched IDs via normalization before dropping the edge.
226
+ if src not in node_set:
227
+ src = norm_to_id.get(_normalize_id(src), src)
228
+ if tgt not in node_set:
229
+ tgt = norm_to_id.get(_normalize_id(tgt), tgt)
230
+ if src not in node_set or tgt not in node_set:
231
+ continue # skip edges to external/stdlib nodes - expected, not an error
232
+ attrs = {k: v for k, v in edge.items() if k not in ("source", "target")}
233
+ if "source_file" in attrs:
234
+ attrs["source_file"] = _norm_source_file(attrs["source_file"], _root)
235
+ # Drop cross-language INFERRED `calls` edges — same short names (render,
236
+ # parse, etc.) appear across language boundaries in multi-language chunks,
237
+ # producing phantom edges that don't represent real call relationships.
238
+ if attrs.get("relation") == "calls" and attrs.get("confidence") == "INFERRED":
239
+ _LANG_FAMILY: dict[str, str] = {
240
+ ".py": "py", ".pyi": "py",
241
+ ".js": "js", ".mjs": "js", ".cjs": "js", ".jsx": "js",
242
+ ".ts": "js", ".tsx": "js",
243
+ ".go": "go", ".rs": "rs",
244
+ ".java": "jvm", ".kt": "jvm", ".scala": "jvm", ".groovy": "jvm",
245
+ ".c": "c", ".h": "c", ".cc": "cpp", ".cpp": "cpp", ".hpp": "cpp",
246
+ ".rb": "rb", ".php": "php", ".cs": "cs", ".swift": "swift", ".lua": "lua",
247
+ }
248
+ src_ext = Path(G.nodes[src].get("source_file") or "").suffix.lower()
249
+ tgt_ext = Path(G.nodes[tgt].get("source_file") or "").suffix.lower()
250
+ if src_ext and tgt_ext and _LANG_FAMILY.get(src_ext) != _LANG_FAMILY.get(tgt_ext):
251
+ continue
252
+ # Preserve original edge direction - undirected graphs lose it otherwise,
253
+ # causing display functions to show edges backwards.
254
+ attrs["_src"] = src
255
+ attrs["_tgt"] = tgt
256
+ # When the graph is undirected and the same node pair appears twice with
257
+ # the same relation but opposite directions (e.g. a `calls` b and b `calls` a),
258
+ # nx.Graph collapses them into one edge. The deterministic sort above means
259
+ # the lexicographically-later direction would systematically overwrite the
260
+ # earlier one's _src/_tgt, silently flipping the surviving edge's caller
261
+ # and callee. First-seen direction wins instead — drop the redundant
262
+ # reverse-direction duplicate so the original direction is preserved (#1061).
263
+ if not G.is_directed() and G.has_edge(src, tgt):
264
+ existing = edge_data(G, src, tgt)
265
+ if existing.get("relation") == attrs.get("relation") and (
266
+ existing.get("_src") == tgt and existing.get("_tgt") == src
267
+ ):
268
+ continue
269
+ G.add_edge(src, tgt, **attrs)
270
+ hyperedges = extraction.get("hyperedges", [])
271
+ if hyperedges:
272
+ G.graph["hyperedges"] = hyperedges
273
+ return G
274
+
275
+
276
+ def build(
277
+ extractions: list[dict],
278
+ *,
279
+ directed: bool = False,
280
+ dedup: bool = True,
281
+ dedup_llm_backend: str | None = None,
282
+ root: str | Path | None = None,
283
+ ) -> nx.Graph:
284
+ """Merge multiple extraction results into one graph.
285
+
286
+ directed=True produces a DiGraph that preserves edge direction (source→target).
287
+ directed=False (default) produces an undirected Graph for backward compatibility.
288
+ dedup=True (default) runs entity deduplication before building the graph.
289
+ dedup_llm_backend: if set (e.g. "gemini", "claude", or "kimi"), uses LLM to resolve
290
+ ambiguous pairs in the 75–92 Jaro-Winkler score zone.
291
+ root: if given, absolute source_file paths are made relative to root (#932).
292
+
293
+ Extractions are merged in order. For nodes with the same ID, the last
294
+ extraction's attributes win (NetworkX add_node overwrites). Pass AST
295
+ results before semantic results so semantic labels take precedence, or
296
+ reverse the order if you prefer AST source_location precision to win.
297
+ """
298
+ from graphify.dedup import deduplicate_entities
299
+ combined: dict = {"nodes": [], "edges": [], "hyperedges": [], "input_tokens": 0, "output_tokens": 0}
300
+ for ext in extractions:
301
+ combined["nodes"].extend(ext.get("nodes", []))
302
+ combined["edges"].extend(ext.get("edges", []))
303
+ combined["hyperedges"].extend(ext.get("hyperedges", []))
304
+ combined["input_tokens"] += ext.get("input_tokens", 0)
305
+ combined["output_tokens"] += ext.get("output_tokens", 0)
306
+ if dedup and combined["nodes"]:
307
+ combined["nodes"], combined["edges"] = deduplicate_entities(
308
+ combined["nodes"], combined["edges"], communities={},
309
+ dedup_llm_backend=dedup_llm_backend,
310
+ )
311
+ return build_from_json(combined, directed=directed, root=root)
312
+
313
+
314
+ def _norm_label(label: str) -> str:
315
+ """Canonical dedup key — Unicode-aware, preserves CJK/word characters."""
316
+ label = unicodedata.normalize("NFKC", label)
317
+ return re.sub(r"[\W_ ]+", " ", label.casefold(), flags=re.UNICODE).strip()
318
+
319
+
320
+ def deduplicate_by_label(nodes: list[dict], edges: list[dict]) -> tuple[list[dict], list[dict]]:
321
+ """Merge nodes that share a normalised label, rewriting edge references.
322
+
323
+ Prefers IDs without chunk suffixes (_c\\d+) and shorter IDs when tied.
324
+ Drops self-loops created by the merge. Called in build() automatically.
325
+ """
326
+ _CHUNK_SUFFIX = re.compile(r"_c\d+$")
327
+ canonical: dict[str, dict] = {} # norm_label -> surviving node
328
+ remap: dict[str, str] = {} # old_id -> surviving_id
329
+
330
+ for node in nodes:
331
+ key = _norm_label(node.get("label", node.get("id", "")))
332
+ if not key:
333
+ continue
334
+ existing = canonical.get(key)
335
+ if existing is None:
336
+ canonical[key] = node
337
+ else:
338
+ has_suffix = bool(_CHUNK_SUFFIX.search(node["id"]))
339
+ existing_has_suffix = bool(_CHUNK_SUFFIX.search(existing["id"]))
340
+ if has_suffix and not existing_has_suffix:
341
+ remap[node["id"]] = existing["id"]
342
+ elif existing_has_suffix and not has_suffix:
343
+ remap[existing["id"]] = node["id"]
344
+ canonical[key] = node
345
+ elif len(node["id"]) < len(existing["id"]):
346
+ remap[existing["id"]] = node["id"]
347
+ canonical[key] = node
348
+ else:
349
+ remap[node["id"]] = existing["id"]
350
+
351
+ if not remap:
352
+ return nodes, edges
353
+
354
+ print(f"[graphify] Deduplicated {len(remap)} duplicate node(s) by label.", file=sys.stderr)
355
+ deduped_nodes = list(canonical.values())
356
+ deduped_edges = []
357
+ for edge in edges:
358
+ e = dict(edge)
359
+ e["source"] = remap.get(e["source"], e["source"])
360
+ e["target"] = remap.get(e["target"], e["target"])
361
+ if e["source"] != e["target"]:
362
+ deduped_edges.append(e)
363
+ return deduped_nodes, deduped_edges
364
+
365
+
366
+ def build_merge(
367
+ new_chunks: list[dict],
368
+ graph_path: str | Path = "graphify-out/graph.json",
369
+ prune_sources: list[str] | None = None,
370
+ *,
371
+ directed: bool = False,
372
+ dedup: bool = True,
373
+ dedup_llm_backend: str | None = None,
374
+ root: str | Path | None = None,
375
+ ) -> nx.Graph:
376
+ """Load existing graph.json, merge new chunks into it, and save back.
377
+
378
+ Never replaces - only grows (or prunes deleted-file nodes via prune_sources).
379
+ Safe to call repeatedly: existing nodes and edges are preserved.
380
+ root: if given, absolute source_file paths in new_chunks are made relative (#932).
381
+ """
382
+ graph_path = Path(graph_path)
383
+ if graph_path.exists():
384
+ # Read JSON directly instead of going through node_link_graph().
385
+ # The latter rebuilds an undirected nx.Graph and then enumerating
386
+ # edges() yields endpoints based on node insertion order, which
387
+ # silently flips directional edges (e.g. `calls`) when the callee
388
+ # was inserted before the caller. The _src/_tgt direction-preserving
389
+ # attrs are popped before saving in export.py, so going through the
390
+ # NetworkX round-trip loses direction permanently (#760).
391
+ from graphify.security import check_graph_file_size_cap
392
+ check_graph_file_size_cap(graph_path)
393
+ data = json.loads(graph_path.read_text(encoding="utf-8"))
394
+ links_key = "links" if "links" in data else "edges"
395
+ existing_nodes = list(data.get("nodes", []))
396
+ existing_edges = list(data.get(links_key, []))
397
+ base = [{"nodes": existing_nodes, "edges": existing_edges}]
398
+ else:
399
+ existing_nodes = []
400
+ base = []
401
+
402
+ all_chunks = base + list(new_chunks)
403
+ G = build(all_chunks, directed=directed, dedup=dedup, dedup_llm_backend=dedup_llm_backend, root=root)
404
+
405
+ # Prune nodes and edges from deleted source files
406
+ if prune_sources:
407
+ # Build a set containing both the raw form (matches nodes that kept
408
+ # absolute source_file) and the normalised relative form (matches nodes
409
+ # that were relativised by _norm_source_file at build time).
410
+ # .resolve() handles symlinked roots and redundant ".." / "./" segments
411
+ # so Path.relative_to() succeeds even when the scan root is a symlink.
412
+ # (#1007: manifest absolute paths vs graph relative source_file mismatch)
413
+ _root_str = str(Path(root).resolve()) if root is not None else None
414
+ prune_set: set[str] = set()
415
+ for p in prune_sources:
416
+ if not p:
417
+ continue
418
+ prune_set.add(p)
419
+ norm = _norm_source_file(p, _root_str)
420
+ if norm:
421
+ prune_set.add(norm)
422
+ to_remove = [
423
+ n for n, d in G.nodes(data=True)
424
+ if d.get("source_file") in prune_set
425
+ ]
426
+ G.remove_nodes_from(to_remove)
427
+ n_files = len(prune_sources)
428
+ n_nodes = len(to_remove)
429
+ if n_nodes:
430
+ print(
431
+ f"[graphify] Pruned {n_nodes} node(s) from {n_files} deleted source file(s).",
432
+ file=sys.stderr,
433
+ )
434
+
435
+ edges_to_remove = [
436
+ (u, v) for u, v, d in G.edges(data=True)
437
+ if d.get("source_file") in prune_set
438
+ ]
439
+ if edges_to_remove:
440
+ G.remove_edges_from(edges_to_remove)
441
+ print(
442
+ f"[graphify] Pruned {len(edges_to_remove)} edge(s) from deleted source file(s).",
443
+ file=sys.stderr,
444
+ )
445
+
446
+ if not n_nodes and not edges_to_remove:
447
+ print(
448
+ f"[graphify] {n_files} source file(s) deleted since last run — "
449
+ f"no matching nodes or edges in graph, already clean.",
450
+ file=sys.stderr,
451
+ )
452
+
453
+ # Safety check: refuse to shrink the graph silently (#479)
454
+ # Skip when dedup or prune_sources is active — shrinkage is intentional there.
455
+ if graph_path.exists() and not dedup and not prune_sources:
456
+ existing_n = len(existing_nodes)
457
+ new_n = G.number_of_nodes()
458
+ if new_n < existing_n:
459
+ raise ValueError(
460
+ f"graphify: build_merge would shrink graph from {existing_n} → {new_n} nodes. "
461
+ f"Pass prune_sources explicitly if you intend to remove nodes."
462
+ )
463
+
464
+ return G
465
+
466
+
467
+ def prefix_graph_for_global(G: nx.Graph, repo_tag: str) -> nx.Graph:
468
+ """Return a copy of G with all node IDs prefixed with repo_tag::.
469
+
470
+ Labels are preserved unchanged (for display). A 'local_id' attribute
471
+ is added to each node so the original ID can be recovered. Edges are
472
+ rewritten to match the new prefixed IDs. The 'repo' attribute is set
473
+ on every node.
474
+ """
475
+ relabel = {n: f"{repo_tag}::{n}" for n in G.nodes}
476
+ H = nx.relabel_nodes(G, relabel, copy=True)
477
+ for node, data in H.nodes(data=True):
478
+ data["repo"] = repo_tag
479
+ data.setdefault("local_id", node.split("::", 1)[1])
480
+ return H
481
+
482
+
483
+ def prune_repo_from_graph(G: nx.Graph, repo_tag: str) -> int:
484
+ """Remove all nodes tagged with repo_tag from G in-place. Returns count removed."""
485
+ to_remove = [n for n, d in G.nodes(data=True) if d.get("repo") == repo_tag]
486
+ G.remove_nodes_from(to_remove)
487
+ return len(to_remove)