@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,70 @@
1
+ """Query logging for graphify — append-only JSONL, fail-silent."""
2
+ from __future__ import annotations
3
+
4
+ import json
5
+ import os
6
+ import re
7
+ import time
8
+ from datetime import datetime, timezone
9
+ from pathlib import Path
10
+ from typing import Any
11
+
12
+ _NODES_RE = re.compile(r"(\d+)\s+nodes?\s+found")
13
+
14
+
15
+ def _log_path() -> Path | None:
16
+ if os.environ.get("GRAPHIFY_QUERY_LOG_DISABLE", "").lower() in ("1", "true", "yes"):
17
+ return None
18
+ override = os.environ.get("GRAPHIFY_QUERY_LOG", "").strip()
19
+ if override:
20
+ return Path(override).expanduser()
21
+ return Path.home() / ".cache" / "graphify-queries.log"
22
+
23
+
24
+ def _log_responses() -> bool:
25
+ return os.environ.get("GRAPHIFY_QUERY_LOG_RESPONSES", "").lower() in ("1", "true", "yes")
26
+
27
+
28
+ def nodes_from_result(result: str) -> int | None:
29
+ m = _NODES_RE.search(result or "")
30
+ return int(m.group(1)) if m else None
31
+
32
+
33
+ def log_query(
34
+ *,
35
+ kind: str,
36
+ question: str,
37
+ corpus: str,
38
+ result: str | None = None,
39
+ nodes_returned: int | None = None,
40
+ duration_ms: float | None = None,
41
+ **extra: Any,
42
+ ) -> None:
43
+ """Append one JSONL record to the query log. Never raises."""
44
+ try:
45
+ path = _log_path()
46
+ if path is None:
47
+ return
48
+ if nodes_returned is None and result is not None:
49
+ nodes_returned = nodes_from_result(result)
50
+ rec: dict[str, Any] = {
51
+ "ts": datetime.now(timezone.utc).isoformat(),
52
+ "kind": kind,
53
+ "question": question,
54
+ "corpus": corpus,
55
+ "nodes_returned": nodes_returned,
56
+ }
57
+ if result is not None:
58
+ rec["result_chars"] = len(result)
59
+ if duration_ms is not None:
60
+ rec["duration_ms"] = round(duration_ms, 3)
61
+ for k, v in extra.items():
62
+ if v is not None:
63
+ rec[k] = v
64
+ if result is not None and _log_responses():
65
+ rec["response"] = result
66
+ path.parent.mkdir(parents=True, exist_ok=True)
67
+ with path.open("a", encoding="utf-8") as fh:
68
+ fh.write(json.dumps(rec, ensure_ascii=False) + "\n")
69
+ except Exception:
70
+ pass
@@ -0,0 +1,218 @@
1
+ # generate GRAPH_REPORT.md - the human-readable audit trail
2
+ from __future__ import annotations
3
+ import re
4
+ from datetime import date
5
+ import networkx as nx
6
+
7
+
8
+ def _safe_community_name(label: str) -> str:
9
+ """Mirrors export.safe_name so community hub filenames and report wikilinks always agree."""
10
+ cleaned = re.sub(r'[\\/*?:"<>|#^[\]]', "", label.replace("\r\n", " ").replace("\r", " ").replace("\n", " ")).strip()
11
+ cleaned = re.sub(r"\.(md|mdx|markdown)$", "", cleaned, flags=re.IGNORECASE)
12
+ return cleaned or "unnamed"
13
+
14
+
15
+ def generate(
16
+ G: nx.Graph,
17
+ communities: dict[int, list[str]],
18
+ cohesion_scores: dict[int, float],
19
+ community_labels: dict[int, str],
20
+ god_node_list: list[dict],
21
+ surprise_list: list[dict],
22
+ detection_result: dict,
23
+ token_cost: dict,
24
+ root: str,
25
+ suggested_questions: list[dict] | None = None,
26
+ min_community_size: int = 3,
27
+ built_at_commit: str | None = None,
28
+ ) -> str:
29
+ today = date.today().isoformat()
30
+
31
+ # JSON deserialization produces string keys; normalize to int so .get(cid) works.
32
+ if community_labels:
33
+ community_labels = {int(k) if isinstance(k, str) else k: v for k, v in community_labels.items()}
34
+
35
+ confidences = [d.get("confidence", "EXTRACTED") for _, _, d in G.edges(data=True)]
36
+ total = len(confidences) or 1
37
+ ext_pct = round(confidences.count("EXTRACTED") / total * 100)
38
+ inf_pct = round(confidences.count("INFERRED") / total * 100)
39
+ amb_pct = round(confidences.count("AMBIGUOUS") / total * 100)
40
+
41
+ inf_edges = [(u, v, d) for u, v, d in G.edges(data=True) if d.get("confidence") == "INFERRED"]
42
+ inf_scores = [d.get("confidence_score", 0.5) for _, _, d in inf_edges]
43
+ inf_avg = round(sum(inf_scores) / len(inf_scores), 2) if inf_scores else None
44
+
45
+ lines = [
46
+ f"# Graph Report - {root} ({today})",
47
+ "",
48
+ "## Corpus Check",
49
+ ]
50
+ if detection_result.get("warning"):
51
+ lines.append(f"- {detection_result['warning']}")
52
+ else:
53
+ lines += [
54
+ f"- {detection_result['total_files']} files · ~{detection_result['total_words']:,} words",
55
+ "- Verdict: corpus is large enough that graph structure adds value.",
56
+ ]
57
+
58
+ from .analyze import _is_file_node as _ifn
59
+ non_empty = {cid: nodes for cid, nodes in communities.items()
60
+ if any(not _ifn(G, n) for n in nodes)}
61
+ thin_count_summary = sum(
62
+ 1 for nodes in communities.values()
63
+ if 0 < sum(1 for n in nodes if not _ifn(G, n)) < min_community_size
64
+ )
65
+ shown_count = len(communities) - thin_count_summary
66
+
67
+ lines += [
68
+ "",
69
+ "## Summary",
70
+ f"- {G.number_of_nodes()} nodes · {G.number_of_edges()} edges · {len(communities)} communities"
71
+ + (f" ({shown_count} shown, {thin_count_summary} thin omitted)" if thin_count_summary else ""),
72
+ f"- Extraction: {ext_pct}% EXTRACTED · {inf_pct}% INFERRED · {amb_pct}% AMBIGUOUS"
73
+ + (f" · INFERRED: {len(inf_edges)} edges (avg confidence: {inf_avg})" if inf_avg is not None else ""),
74
+ f"- Token cost: {token_cost.get('input', 0):,} input · {token_cost.get('output', 0):,} output",
75
+ ]
76
+
77
+ if built_at_commit:
78
+ lines += [
79
+ "",
80
+ "## Graph Freshness",
81
+ f"- Built from commit: `{built_at_commit[:8]}`",
82
+ "- Run `git rev-parse HEAD` and compare to check if the graph is stale.",
83
+ "- Run `graphify update .` after code changes (no API cost).",
84
+ ]
85
+
86
+ # Community hub navigation - links to _COMMUNITY_*.md files in the Obsidian vault.
87
+ # Without these, GRAPH_REPORT.md is a dead-end and the vault splits into disconnected components.
88
+ if non_empty:
89
+ lines += ["", "## Community Hubs (Navigation)"]
90
+ for cid in non_empty:
91
+ label = community_labels.get(cid, f"Community {cid}")
92
+ safe = _safe_community_name(label)
93
+ lines.append(f"- [[_COMMUNITY_{safe}|{label}]]")
94
+
95
+ lines += [
96
+ "",
97
+ "## God Nodes (most connected - your core abstractions)",
98
+ ]
99
+ for i, node in enumerate(god_node_list, 1):
100
+ lines.append(f"{i}. `{node['label']}` - {node['degree']} edges")
101
+
102
+ lines += ["", "## Surprising Connections (you probably didn't know these)"]
103
+ if surprise_list:
104
+ for s in surprise_list:
105
+ relation = s.get("relation", "related_to")
106
+ note = s.get("note", "")
107
+ files = s.get("source_files", ["", ""])
108
+ conf = s.get("confidence", "EXTRACTED")
109
+ cscore = s.get("confidence_score")
110
+ if conf == "INFERRED" and cscore is not None:
111
+ conf_tag = f"INFERRED {cscore:.2f}"
112
+ else:
113
+ conf_tag = conf
114
+ sem_tag = " [semantically similar]" if relation == "semantically_similar_to" else ""
115
+ lines += [
116
+ f"- `{s['source']}` --{relation}--> `{s['target']}` [{conf_tag}]{sem_tag}",
117
+ f" {files[0]} → {files[1]}" + (f" _{note}_" if note else ""),
118
+ ]
119
+ else:
120
+ lines.append("- None detected - all connections are within the same source files.")
121
+
122
+ # Circular imports surfaced from file-level dependency graph.
123
+ from .analyze import find_import_cycles
124
+ cycles = find_import_cycles(G)
125
+ lines += ["", "## Import Cycles"]
126
+ if cycles:
127
+ for c in cycles:
128
+ cycle = c.get("cycle", [])
129
+ length = c.get("length", len(cycle))
130
+ if not cycle:
131
+ continue
132
+ cycle_path = " -> ".join(cycle + [cycle[0]])
133
+ lines.append(f"- {length}-file cycle: `{cycle_path}`")
134
+ else:
135
+ lines.append("- None detected.")
136
+
137
+ hyperedges = G.graph.get("hyperedges", [])
138
+ if hyperedges:
139
+ lines += ["", "## Hyperedges (group relationships)"]
140
+ for h in hyperedges:
141
+ node_labels = ", ".join(h.get("nodes", []))
142
+ conf = h.get("confidence", "INFERRED")
143
+ cscore = h.get("confidence_score")
144
+ conf_tag = f"{conf} {cscore:.2f}" if cscore is not None else conf
145
+ lines.append(f"- **{h.get('label', h.get('id', ''))}** — {node_labels} [{conf_tag}]")
146
+
147
+ lines += ["", f"## Communities ({len(communities)} total, {thin_count_summary} thin omitted)"]
148
+ for cid, nodes in communities.items():
149
+ label = community_labels.get(cid, f"Community {cid}")
150
+ score = cohesion_scores.get(cid, 0.0)
151
+ # Filter method/function stubs from display - they're structural noise
152
+ real_nodes = [n for n in nodes if not _ifn(G, n)]
153
+ if not real_nodes:
154
+ continue
155
+ if len(real_nodes) < min_community_size:
156
+ continue
157
+ display = [G.nodes[n].get("label", n) for n in real_nodes[:8]]
158
+ suffix = f" (+{len(real_nodes)-8} more)" if len(real_nodes) > 8 else ""
159
+ lines += [
160
+ "",
161
+ f"### Community {cid} - \"{label}\"",
162
+ f"Cohesion: {score:.2f}",
163
+ f"Nodes ({len(real_nodes)}): {', '.join(display)}{suffix}",
164
+ ]
165
+
166
+ ambiguous = [(u, v, d) for u, v, d in G.edges(data=True) if d.get("confidence") == "AMBIGUOUS"]
167
+ if ambiguous:
168
+ lines += ["", "## Ambiguous Edges - Review These"]
169
+ for u, v, d in ambiguous:
170
+ ul = G.nodes[u].get("label", u)
171
+ vl = G.nodes[v].get("label", v)
172
+ lines += [
173
+ f"- `{ul}` → `{vl}` [AMBIGUOUS]",
174
+ f" {d.get('source_file', '')} · relation: {d.get('relation', 'unknown')}",
175
+ ]
176
+
177
+ # --- Gaps section ---
178
+ from .analyze import _is_file_node, _is_concept_node
179
+
180
+ isolated = [
181
+ n for n in G.nodes()
182
+ if G.degree(n) <= 1
183
+ and not _is_file_node(G, n)
184
+ and not _is_concept_node(G, n)
185
+ and G.nodes[n].get("file_type") != "rationale"
186
+ ]
187
+ thin_communities = {
188
+ cid: nodes for cid, nodes in communities.items()
189
+ if 0 < sum(1 for n in nodes if not _is_file_node(G, n)) < 3
190
+ }
191
+ gap_count = len(isolated) + len(thin_communities)
192
+
193
+ if gap_count > 0 or amb_pct > 20:
194
+ lines += ["", "## Knowledge Gaps"]
195
+ if isolated:
196
+ isolated_labels = [G.nodes[n].get("label", n) for n in isolated[:5]]
197
+ suffix = f" (+{len(isolated)-5} more)" if len(isolated) > 5 else ""
198
+ lines.append(f"- **{len(isolated)} isolated node(s):** {', '.join(f'`{l}`' for l in isolated_labels)}{suffix}")
199
+ lines.append(" These have ≤1 connection - possible missing edges or undocumented components.")
200
+ if thin_communities:
201
+ lines.append(f"- **{len(thin_communities)} thin communities (<{min_community_size} nodes) omitted from report** — run `graphify query` to explore isolated nodes.")
202
+ if amb_pct > 20:
203
+ lines.append(f"- **High ambiguity: {amb_pct}% of edges are AMBIGUOUS.** Review the Ambiguous Edges section above.")
204
+
205
+ if suggested_questions:
206
+ lines += ["", "## Suggested Questions"]
207
+ no_signal = len(suggested_questions) == 1 and suggested_questions[0].get("type") == "no_signal"
208
+ if no_signal:
209
+ lines.append(f"_{suggested_questions[0]['why']}_")
210
+ else:
211
+ lines.append("_Questions this graph is uniquely positioned to answer:_")
212
+ lines.append("")
213
+ for q in suggested_questions:
214
+ if q.get("question"):
215
+ lines.append(f"- **{q['question']}**")
216
+ lines.append(f" _{q['why']}_")
217
+
218
+ return "\n".join(lines)
@@ -0,0 +1,363 @@
1
+ """scip_ingest.py — SCIP JSON ingestion (simplified subset).
2
+
3
+ Reads a simplified SCIP-style JSON structure and converts it into
4
+ Graphify nodes and edges. NOT a full SCIP protobuf implementation —
5
+ this is a skeleton that consumes the simplified shape described below.
6
+
7
+ Not wired to the CLI in this phase.
8
+
9
+ Entry point:
10
+ ingest_scip_json(doc: object, source_file: str = "",
11
+ language: str = "python") -> dict[str, Any]
12
+
13
+ Returns {"nodes": [...], "edges": [...]} compatible with Graphify's
14
+ extraction result format. All edges emitted are endpoint-safe — the
15
+ function builds a symbol → node_id index in a first pass and either
16
+ resolves relationship targets via that index or creates a stub
17
+ external node so `build_from_json()` will keep the edge.
18
+
19
+ Supported (simplified) JSON shape:
20
+ documents[]: { relative_path, language, symbols[] }
21
+ symbols[]: { symbol, kind, display_name, documentation[],
22
+ relationships[], occurrences[] }
23
+ relationships[]: { symbol, is_reference, is_implementation,
24
+ is_type_definition, is_definition }
25
+ occurrences[]: { range[], symbol, symbol_roles }
26
+
27
+ This shape diverges from the official SCIP protobuf (where occurrences
28
+ live on the document, not on each symbol). We consume the simplified
29
+ shape that LLM-generated SCIP-style JSON commonly produces. Future
30
+ cycles may add document-level occurrence support.
31
+ """
32
+
33
+ from __future__ import annotations
34
+
35
+ import hashlib
36
+ import re
37
+ from typing import Any
38
+
39
+ from graphify.security import sanitize_metadata
40
+
41
+
42
+ def ingest_scip_json(
43
+ doc: object,
44
+ source_file: str = "",
45
+ language: str = "python",
46
+ ) -> dict[str, Any]:
47
+ """Convert a SCIP-style JSON document into Graphify nodes and edges.
48
+
49
+ Parameter ``doc`` is ``object`` (not ``dict[str, Any]``) because SCIP
50
+ documents come from external tools — we may be handed arbitrary
51
+ deserialized JSON. The first check rejects anything that isn't a dict
52
+ and returns the empty result.
53
+
54
+ Two-pass design:
55
+ 1. Build a ``symbol_str → node_id`` index across every valid symbol
56
+ in every valid document, plus collect per-symbol metadata.
57
+ 2. Emit nodes for every indexed symbol and then emit relationship
58
+ edges. Relationship targets are resolved via the index when
59
+ present; otherwise a stub ``scip_external`` node is added so
60
+ edges never dangle.
61
+ """
62
+ nodes: list[dict[str, Any]] = []
63
+ edges: list[dict[str, Any]] = []
64
+ seen_node_ids: set[str] = set()
65
+ seen_edges: set[tuple[str, str, str, str | None]] = set()
66
+
67
+ if not isinstance(doc, dict):
68
+ return {"nodes": nodes, "edges": edges}
69
+
70
+ documents = doc.get("documents", [])
71
+ if not isinstance(documents, list):
72
+ return {"nodes": nodes, "edges": edges}
73
+
74
+ # ---- pass 1: build symbol → node_id indices -----------------------------
75
+ # Two indices so relationship resolution can be document-aware:
76
+ # per_doc: (symbol_id, doc_path) → node_id (same-document precedence)
77
+ # global: symbol_id → list[node_id] (cross-document fallback,
78
+ # used only when unambiguous)
79
+ per_doc_index: dict[tuple[str, str], str] = {}
80
+ global_index: dict[str, list[str]] = {}
81
+ # Per-symbol metadata kept for pass-2 node emission (avoids re-walking
82
+ # the document tree).
83
+ symbol_records: list[dict[str, Any]] = []
84
+ for document in documents:
85
+ if not isinstance(document, dict):
86
+ continue
87
+ doc_path = _coerce_str(document.get("relative_path"), source_file)
88
+ doc_language = _coerce_str(document.get("language"), language)
89
+ symbols = document.get("symbols", [])
90
+ if not isinstance(symbols, list):
91
+ continue
92
+ for symbol in symbols:
93
+ if not isinstance(symbol, dict):
94
+ continue
95
+ symbol_id = _coerce_str(symbol.get("symbol"), "")
96
+ if not symbol_id:
97
+ continue
98
+ node_id = _make_scip_node_id(symbol_id, doc_path)
99
+ per_doc_index.setdefault((symbol_id, doc_path), node_id)
100
+ # Dedupe node_ids in the global index — duplicate symbol records
101
+ # within the SAME document produce identical node_ids, and we
102
+ # don't want them to look like cross-document ambiguity.
103
+ candidates = global_index.setdefault(symbol_id, [])
104
+ if node_id not in candidates:
105
+ candidates.append(node_id)
106
+ symbol_records.append(
107
+ {
108
+ "node_id": node_id,
109
+ "symbol_id": symbol_id,
110
+ "doc_path": doc_path,
111
+ "language": doc_language,
112
+ "raw": symbol,
113
+ }
114
+ )
115
+
116
+ # ---- pass 2: emit nodes + relationship edges -----------------------------
117
+ for record in symbol_records:
118
+ _emit_symbol_node(record, nodes, seen_node_ids)
119
+ _emit_relationships(
120
+ record,
121
+ per_doc_index,
122
+ global_index,
123
+ nodes,
124
+ edges,
125
+ seen_node_ids,
126
+ seen_edges,
127
+ )
128
+
129
+ return {"nodes": nodes, "edges": edges}
130
+
131
+
132
+ def _emit_symbol_node(
133
+ record: dict[str, Any],
134
+ nodes: list[dict[str, Any]],
135
+ seen_node_ids: set[str],
136
+ ) -> None:
137
+ """Append the canonical node for a SCIP symbol record."""
138
+ node_id = record["node_id"]
139
+ if node_id in seen_node_ids:
140
+ return
141
+ raw = record["raw"]
142
+ symbol_id = record["symbol_id"]
143
+ doc_path = record["doc_path"]
144
+ kind = _coerce_str(raw.get("kind"), "unknown")
145
+ display_name = _coerce_str(raw.get("display_name"), "")
146
+ documentation = raw.get("documentation", [])
147
+ description = ""
148
+ if isinstance(documentation, list) and documentation:
149
+ first = documentation[0]
150
+ if isinstance(first, str):
151
+ description = first
152
+ occurrences = raw.get("occurrences", [])
153
+ sourceline = _first_occurrence_line(occurrences)
154
+ suffix = symbol_id.split("#")[-1] if "#" in symbol_id else symbol_id
155
+ label = display_name or suffix or symbol_id
156
+ seen_node_ids.add(node_id) # label uses display_name or suffix (never empty for valid symbols)
157
+ nodes.append(
158
+ {
159
+ "id": node_id,
160
+ "label": label,
161
+ "file_type": _scip_kind_to_file_type(kind),
162
+ "source_file": doc_path,
163
+ "source_location": f"L{sourceline}" if sourceline else "",
164
+ "metadata": sanitize_metadata(_build_scip_metadata(symbol_id, kind, description)),
165
+ }
166
+ )
167
+
168
+
169
+ def _emit_relationships(
170
+ record: dict[str, Any],
171
+ per_doc_index: dict[tuple[str, str], str],
172
+ global_index: dict[str, list[str]],
173
+ nodes: list[dict[str, Any]],
174
+ edges: list[dict[str, Any]],
175
+ seen_node_ids: set[str],
176
+ seen_edges: set[tuple[str, str, str, str | None]],
177
+ ) -> None:
178
+ """Append edges (and stub nodes when needed) for a symbol's relationships.
179
+
180
+ Relationship target resolution order:
181
+ 1. Same-document `(target_symbol, doc_path)` — duplicate local symbol
182
+ names across files route to THIS file's symbol, not another's.
183
+ 2. Unique cross-document match — when the symbol exists in exactly
184
+ one document and that document is different from the source.
185
+ 3. Stub external node — for symbols not declared in any document
186
+ OR ambiguous duplicates across multiple documents (refusing to
187
+ guess silently).
188
+ """
189
+ raw = record["raw"]
190
+ source_node_id = record["node_id"]
191
+ doc_path = record["doc_path"]
192
+ occurrences = raw.get("occurrences", [])
193
+ sourceline = _first_occurrence_line(occurrences)
194
+ relationships = raw.get("relationships")
195
+ if not isinstance(relationships, list):
196
+ return
197
+ for rel in relationships:
198
+ if not isinstance(rel, dict):
199
+ continue
200
+ target_symbol = _coerce_str(rel.get("symbol"), "")
201
+ if not target_symbol:
202
+ continue
203
+ target_node_id = _resolve_relationship_target(
204
+ target_symbol,
205
+ doc_path,
206
+ per_doc_index,
207
+ global_index,
208
+ )
209
+ if target_node_id is None:
210
+ # External relationship target: emit a stub node so the edge
211
+ # is never dangling. The stub uses the source document's path
212
+ # as its host context.
213
+ target_node_id = _make_scip_node_id(target_symbol, doc_path)
214
+ if target_node_id not in seen_node_ids:
215
+ seen_node_ids.add(target_node_id)
216
+ suffix = target_symbol.split("#")[-1] if "#" in target_symbol else target_symbol
217
+ nodes.append(
218
+ {
219
+ "id": target_node_id,
220
+ "label": suffix or target_symbol,
221
+ "file_type": "code",
222
+ "source_file": doc_path,
223
+ "source_location": "",
224
+ "metadata": sanitize_metadata(
225
+ _build_scip_metadata(target_symbol, "external", "")
226
+ ),
227
+ }
228
+ )
229
+ relation = _scip_relation_for(rel)
230
+ source_location = f"L{sourceline}" if sourceline else ""
231
+ key = (source_node_id, target_node_id, relation, source_location)
232
+ if key in seen_edges:
233
+ continue
234
+ seen_edges.add(key)
235
+ edges.append(
236
+ {
237
+ "source": source_node_id,
238
+ "target": target_node_id,
239
+ "relation": relation,
240
+ "confidence": "EXTRACTED",
241
+ "confidence_score": 1.0,
242
+ "source_file": doc_path,
243
+ "source_location": source_location,
244
+ "weight": 1.0,
245
+ "context": "scip",
246
+ "metadata": sanitize_metadata({"scip_relationship": rel}),
247
+ }
248
+ )
249
+
250
+
251
+ def _resolve_relationship_target(
252
+ target_symbol: str,
253
+ source_doc_path: str,
254
+ per_doc_index: dict[tuple[str, str], str],
255
+ global_index: dict[str, list[str]],
256
+ ) -> str | None:
257
+ """Resolve a SCIP relationship target to an emitted node id, or None.
258
+
259
+ Resolution order:
260
+ 1. Same-document match — `(target_symbol, source_doc_path)`.
261
+ 2. Unique cross-document match — exactly one node id in the global
262
+ index for this symbol AND it isn't the same document we already
263
+ tried.
264
+ 3. None — symbol is either absent globally OR ambiguous (defined in
265
+ multiple documents). The caller emits a stub external node.
266
+ """
267
+ same_doc = per_doc_index.get((target_symbol, source_doc_path))
268
+ if same_doc is not None:
269
+ return same_doc
270
+ candidates = global_index.get(target_symbol, [])
271
+ if len(candidates) == 1:
272
+ return candidates[0]
273
+ return None
274
+
275
+
276
+ def _is_true(value: object) -> bool:
277
+ """Return True only when value is exactly the boolean True.
278
+
279
+ Used for SCIP relationship flags. Truthy strings like ``"false"`` are
280
+ common in untrusted external JSON and must NOT count as a set flag.
281
+ """
282
+ return value is True
283
+
284
+
285
+ def _scip_relation_for(rel: dict[str, Any]) -> str:
286
+ """Pick the Graphify relation tag for a SCIP relationship dict.
287
+
288
+ Flags are accepted only when the value is exactly ``True`` — protects
289
+ against truthy-but-misleading values like ``"false"`` in external JSON.
290
+ """
291
+ if _is_true(rel.get("is_implementation")):
292
+ return "scip_impl"
293
+ if _is_true(rel.get("is_type_definition")):
294
+ return "scip_typed"
295
+ if _is_true(rel.get("is_definition")):
296
+ return "scip_def"
297
+ return "scip_ref"
298
+
299
+
300
+ def _first_occurrence_line(occurrences: object) -> int:
301
+ """Read the 1-based line number from the first occurrence range, defensively.
302
+
303
+ Note: ``bool`` is a subclass of ``int`` in Python — ``isinstance(True, int)``
304
+ is True. We explicitly exclude booleans so a malformed ``range: [True, …]``
305
+ cannot produce ``source_location = "LTrue"``.
306
+ """
307
+ if not isinstance(occurrences, list) or not occurrences:
308
+ return 0
309
+ first = occurrences[0]
310
+ if not isinstance(first, dict):
311
+ return 0
312
+ rng = first.get("range", [])
313
+ if not isinstance(rng, list) or len(rng) < 1:
314
+ return 0
315
+ line = rng[0]
316
+ if isinstance(line, bool) or not isinstance(line, int) or line < 0:
317
+ return 0
318
+ return line
319
+
320
+
321
+ def _coerce_str(value: object, default: str) -> str:
322
+ """Return ``value`` if it is a string, else the ``default`` (also a string)."""
323
+ if isinstance(value, str):
324
+ return value
325
+ if isinstance(default, str):
326
+ return default
327
+ return ""
328
+
329
+
330
+ def _make_scip_node_id(symbol: str, source_file: str) -> str:
331
+ """Derive a stable Graphify node ID from a SCIP symbol identifier.
332
+
333
+ Uses SHA-1 truncated to 12 hex chars (48 bits). This is an identifier,
334
+ not a security boundary — collision risk is acceptable at this scale
335
+ given the per-document scoping prefix.
336
+ """
337
+ raw = f"{source_file}:{symbol}"
338
+ h = hashlib.sha1(raw.encode(), usedforsecurity=False).hexdigest()[:12]
339
+ parts = symbol.split("#")
340
+ suffix = parts[-1] if parts else symbol
341
+ suffix = re.sub(r"[^a-zA-Z0-9_]", "_", suffix).strip("_").lower()
342
+ if suffix:
343
+ return f"scip_{suffix}_{h}"
344
+ return f"scip_{h}"
345
+
346
+
347
+ def _scip_kind_to_file_type(kind: str) -> str:
348
+ """Map SCIP symbol kind to a Graphify file_type."""
349
+ # All SCIP symbols are code entities (functions, methods, classes, …);
350
+ # the `kind` is preserved in metadata for downstream consumers.
351
+ _ = kind # acknowledged but not currently used for file_type routing
352
+ return "code"
353
+
354
+
355
+ def _build_scip_metadata(symbol_id: str, kind: str, description: str) -> dict[str, str]:
356
+ """Build metadata for a SCIP node."""
357
+ meta: dict[str, str] = {
358
+ "scip_symbol": symbol_id,
359
+ "scip_kind": kind,
360
+ }
361
+ if description:
362
+ meta["scip_description"] = description
363
+ return meta