gitnexus 1.6.3-rc.8 → 1.6.3

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 (285) hide show
  1. package/README.md +21 -5
  2. package/dist/_shared/graph/types.d.ts +16 -0
  3. package/dist/_shared/graph/types.d.ts.map +1 -1
  4. package/dist/_shared/index.d.ts +20 -2
  5. package/dist/_shared/index.d.ts.map +1 -1
  6. package/dist/_shared/index.js +11 -0
  7. package/dist/_shared/index.js.map +1 -1
  8. package/dist/_shared/scope-resolution/def-index.js +2 -2
  9. package/dist/_shared/scope-resolution/def-index.js.map +1 -1
  10. package/dist/_shared/scope-resolution/method-dispatch-index.d.ts +8 -0
  11. package/dist/_shared/scope-resolution/method-dispatch-index.d.ts.map +1 -1
  12. package/dist/_shared/scope-resolution/method-dispatch-index.js +2 -2
  13. package/dist/_shared/scope-resolution/method-dispatch-index.js.map +1 -1
  14. package/dist/_shared/scope-resolution/module-scope-index.d.ts +8 -0
  15. package/dist/_shared/scope-resolution/module-scope-index.d.ts.map +1 -1
  16. package/dist/_shared/scope-resolution/module-scope-index.js +10 -2
  17. package/dist/_shared/scope-resolution/module-scope-index.js.map +1 -1
  18. package/dist/_shared/scope-resolution/parsed-file.d.ts +76 -0
  19. package/dist/_shared/scope-resolution/parsed-file.d.ts.map +1 -0
  20. package/dist/_shared/scope-resolution/parsed-file.js +54 -0
  21. package/dist/_shared/scope-resolution/parsed-file.js.map +1 -0
  22. package/dist/_shared/scope-resolution/position-index.d.ts +12 -0
  23. package/dist/_shared/scope-resolution/position-index.d.ts.map +1 -1
  24. package/dist/_shared/scope-resolution/position-index.js +2 -2
  25. package/dist/_shared/scope-resolution/position-index.js.map +1 -1
  26. package/dist/_shared/scope-resolution/qualified-name-index.js +2 -2
  27. package/dist/_shared/scope-resolution/qualified-name-index.js.map +1 -1
  28. package/dist/_shared/scope-resolution/reference-site.d.ts +75 -0
  29. package/dist/_shared/scope-resolution/reference-site.d.ts.map +1 -0
  30. package/dist/_shared/scope-resolution/reference-site.js +24 -0
  31. package/dist/_shared/scope-resolution/reference-site.js.map +1 -0
  32. package/dist/_shared/scope-resolution/registries/class-registry.d.ts +27 -0
  33. package/dist/_shared/scope-resolution/registries/class-registry.d.ts.map +1 -0
  34. package/dist/_shared/scope-resolution/registries/class-registry.js +30 -0
  35. package/dist/_shared/scope-resolution/registries/class-registry.js.map +1 -0
  36. package/dist/_shared/scope-resolution/registries/context.d.ts +69 -0
  37. package/dist/_shared/scope-resolution/registries/context.d.ts.map +1 -0
  38. package/dist/_shared/scope-resolution/registries/context.js +44 -0
  39. package/dist/_shared/scope-resolution/registries/context.js.map +1 -0
  40. package/dist/_shared/scope-resolution/registries/evidence.d.ts +56 -0
  41. package/dist/_shared/scope-resolution/registries/evidence.d.ts.map +1 -0
  42. package/dist/_shared/scope-resolution/registries/evidence.js +150 -0
  43. package/dist/_shared/scope-resolution/registries/evidence.js.map +1 -0
  44. package/dist/_shared/scope-resolution/registries/field-registry.d.ts +26 -0
  45. package/dist/_shared/scope-resolution/registries/field-registry.d.ts.map +1 -0
  46. package/dist/_shared/scope-resolution/registries/field-registry.js +31 -0
  47. package/dist/_shared/scope-resolution/registries/field-registry.js.map +1 -0
  48. package/dist/_shared/scope-resolution/registries/lookup-core.d.ts +81 -0
  49. package/dist/_shared/scope-resolution/registries/lookup-core.d.ts.map +1 -0
  50. package/dist/_shared/scope-resolution/registries/lookup-core.js +332 -0
  51. package/dist/_shared/scope-resolution/registries/lookup-core.js.map +1 -0
  52. package/dist/_shared/scope-resolution/registries/lookup-qualified.d.ts +33 -0
  53. package/dist/_shared/scope-resolution/registries/lookup-qualified.d.ts.map +1 -0
  54. package/dist/_shared/scope-resolution/registries/lookup-qualified.js +56 -0
  55. package/dist/_shared/scope-resolution/registries/lookup-qualified.js.map +1 -0
  56. package/dist/_shared/scope-resolution/registries/method-registry.d.ts +36 -0
  57. package/dist/_shared/scope-resolution/registries/method-registry.d.ts.map +1 -0
  58. package/dist/_shared/scope-resolution/registries/method-registry.js +32 -0
  59. package/dist/_shared/scope-resolution/registries/method-registry.js.map +1 -0
  60. package/dist/_shared/scope-resolution/registries/tie-breaks.d.ts +43 -0
  61. package/dist/_shared/scope-resolution/registries/tie-breaks.d.ts.map +1 -0
  62. package/dist/_shared/scope-resolution/registries/tie-breaks.js +60 -0
  63. package/dist/_shared/scope-resolution/registries/tie-breaks.js.map +1 -0
  64. package/dist/_shared/scope-resolution/resolve-type-ref.d.ts +1 -10
  65. package/dist/_shared/scope-resolution/resolve-type-ref.d.ts.map +1 -1
  66. package/dist/_shared/scope-resolution/resolve-type-ref.js +6 -0
  67. package/dist/_shared/scope-resolution/resolve-type-ref.js.map +1 -1
  68. package/dist/_shared/scope-resolution/scope-tree.d.ts +4 -4
  69. package/dist/_shared/scope-resolution/scope-tree.d.ts.map +1 -1
  70. package/dist/_shared/scope-resolution/scope-tree.js +3 -2
  71. package/dist/_shared/scope-resolution/scope-tree.js.map +1 -1
  72. package/dist/_shared/scope-resolution/shadow/aggregate.d.ts +6 -2
  73. package/dist/_shared/scope-resolution/shadow/aggregate.d.ts.map +1 -1
  74. package/dist/_shared/scope-resolution/shadow/aggregate.js +5 -0
  75. package/dist/_shared/scope-resolution/shadow/aggregate.js.map +1 -1
  76. package/dist/_shared/scope-resolution/types.d.ts +11 -0
  77. package/dist/_shared/scope-resolution/types.d.ts.map +1 -1
  78. package/dist/cli/ai-context.js +35 -4
  79. package/dist/cli/analyze.d.ts +27 -0
  80. package/dist/cli/analyze.js +31 -1
  81. package/dist/cli/clean.js +19 -1
  82. package/dist/cli/group.js +73 -0
  83. package/dist/cli/index-repo.js +8 -1
  84. package/dist/cli/index.js +26 -1
  85. package/dist/cli/list.js +11 -1
  86. package/dist/cli/remove.d.ts +30 -0
  87. package/dist/cli/remove.js +99 -0
  88. package/dist/cli/setup.js +185 -57
  89. package/dist/cli/tool.d.ts +5 -0
  90. package/dist/cli/tool.js +42 -0
  91. package/dist/config/ignore-service.d.ts +9 -0
  92. package/dist/config/ignore-service.js +80 -13
  93. package/dist/core/embedding-mode.d.ts +30 -0
  94. package/dist/core/embedding-mode.js +30 -0
  95. package/dist/core/embeddings/ast-utils.js +22 -22
  96. package/dist/core/embeddings/chunker.js +30 -25
  97. package/dist/core/embeddings/embedding-pipeline.d.ts +6 -0
  98. package/dist/core/embeddings/embedding-pipeline.js +15 -6
  99. package/dist/core/embeddings/text-generator.d.ts +1 -1
  100. package/dist/core/embeddings/text-generator.js +33 -24
  101. package/dist/core/embeddings/types.d.ts +43 -1
  102. package/dist/core/embeddings/types.js +101 -29
  103. package/dist/core/git-staleness.d.ts +18 -0
  104. package/dist/core/git-staleness.js +108 -0
  105. package/dist/core/graph/graph.js +115 -20
  106. package/dist/core/graph/types.d.ts +12 -1
  107. package/dist/core/group/config-parser.d.ts +4 -0
  108. package/dist/core/group/config-parser.js +18 -1
  109. package/dist/core/group/cross-impact.d.ts +41 -0
  110. package/dist/core/group/cross-impact.js +441 -0
  111. package/dist/core/group/extractors/http-patterns/php.js +126 -18
  112. package/dist/core/group/group-path-utils.d.ts +17 -0
  113. package/dist/core/group/group-path-utils.js +40 -0
  114. package/dist/core/group/resolve-at-member.d.ts +10 -0
  115. package/dist/core/group/resolve-at-member.js +31 -0
  116. package/dist/core/group/service.d.ts +9 -0
  117. package/dist/core/group/service.js +259 -25
  118. package/dist/core/group/types.d.ts +30 -0
  119. package/dist/core/ingestion/ast-cache.d.ts +16 -1
  120. package/dist/core/ingestion/ast-cache.js +14 -2
  121. package/dist/core/ingestion/call-processor.js +9 -0
  122. package/dist/core/ingestion/emit-references.d.ts +88 -0
  123. package/dist/core/ingestion/emit-references.js +229 -0
  124. package/dist/core/ingestion/filesystem-walker.js +6 -4
  125. package/dist/core/ingestion/finalize-orchestrator.d.ts +63 -0
  126. package/dist/core/ingestion/finalize-orchestrator.js +139 -0
  127. package/dist/core/ingestion/framework-detection.js +6 -2
  128. package/dist/core/ingestion/import-processor.js +4 -0
  129. package/dist/core/ingestion/import-resolvers/python.js +9 -6
  130. package/dist/core/ingestion/import-target-adapter.d.ts +73 -0
  131. package/dist/core/ingestion/import-target-adapter.js +95 -0
  132. package/dist/core/ingestion/language-provider.d.ts +36 -33
  133. package/dist/core/ingestion/languages/csharp/accessor-unwrap.d.ts +21 -0
  134. package/dist/core/ingestion/languages/csharp/accessor-unwrap.js +56 -0
  135. package/dist/core/ingestion/languages/csharp/arity-metadata.d.ts +26 -0
  136. package/dist/core/ingestion/languages/csharp/arity-metadata.js +46 -0
  137. package/dist/core/ingestion/languages/csharp/arity.d.ts +23 -0
  138. package/dist/core/ingestion/languages/csharp/arity.js +37 -0
  139. package/dist/core/ingestion/languages/csharp/cache-stats.d.ts +15 -0
  140. package/dist/core/ingestion/languages/csharp/cache-stats.js +26 -0
  141. package/dist/core/ingestion/languages/csharp/captures.d.ts +19 -0
  142. package/dist/core/ingestion/languages/csharp/captures.js +249 -0
  143. package/dist/core/ingestion/languages/csharp/import-decomposer.d.ts +19 -0
  144. package/dist/core/ingestion/languages/csharp/import-decomposer.js +93 -0
  145. package/dist/core/ingestion/languages/csharp/import-target.d.ts +25 -0
  146. package/dist/core/ingestion/languages/csharp/import-target.js +123 -0
  147. package/dist/core/ingestion/languages/csharp/index.d.ts +82 -0
  148. package/dist/core/ingestion/languages/csharp/index.js +82 -0
  149. package/dist/core/ingestion/languages/csharp/interpret.d.ts +15 -0
  150. package/dist/core/ingestion/languages/csharp/interpret.js +132 -0
  151. package/dist/core/ingestion/languages/csharp/merge-bindings.d.ts +27 -0
  152. package/dist/core/ingestion/languages/csharp/merge-bindings.js +55 -0
  153. package/dist/core/ingestion/languages/csharp/namespace-siblings.d.ts +50 -0
  154. package/dist/core/ingestion/languages/csharp/namespace-siblings.js +374 -0
  155. package/dist/core/ingestion/languages/csharp/query.d.ts +35 -0
  156. package/dist/core/ingestion/languages/csharp/query.js +515 -0
  157. package/dist/core/ingestion/languages/csharp/receiver-binding.d.ts +31 -0
  158. package/dist/core/ingestion/languages/csharp/receiver-binding.js +135 -0
  159. package/dist/core/ingestion/languages/csharp/scope-resolver.d.ts +10 -0
  160. package/dist/core/ingestion/languages/csharp/scope-resolver.js +63 -0
  161. package/dist/core/ingestion/languages/csharp/simple-hooks.d.ts +53 -0
  162. package/dist/core/ingestion/languages/csharp/simple-hooks.js +76 -0
  163. package/dist/core/ingestion/languages/csharp.js +14 -0
  164. package/dist/core/ingestion/languages/python/arity-metadata.d.ts +24 -0
  165. package/dist/core/ingestion/languages/python/arity-metadata.js +45 -0
  166. package/dist/core/ingestion/languages/python/arity.d.ts +22 -0
  167. package/dist/core/ingestion/languages/python/arity.js +38 -0
  168. package/dist/core/ingestion/languages/python/cache-stats.d.ts +17 -0
  169. package/dist/core/ingestion/languages/python/cache-stats.js +28 -0
  170. package/dist/core/ingestion/languages/python/captures.d.ts +19 -0
  171. package/dist/core/ingestion/languages/python/captures.js +106 -0
  172. package/dist/core/ingestion/languages/python/import-decomposer.d.ts +15 -0
  173. package/dist/core/ingestion/languages/python/import-decomposer.js +112 -0
  174. package/dist/core/ingestion/languages/python/import-target.d.ts +21 -0
  175. package/dist/core/ingestion/languages/python/import-target.js +99 -0
  176. package/dist/core/ingestion/languages/python/index.d.ts +80 -0
  177. package/dist/core/ingestion/languages/python/index.js +80 -0
  178. package/dist/core/ingestion/languages/python/interpret.d.ts +15 -0
  179. package/dist/core/ingestion/languages/python/interpret.js +191 -0
  180. package/dist/core/ingestion/languages/python/merge-bindings.d.ts +16 -0
  181. package/dist/core/ingestion/languages/python/merge-bindings.js +44 -0
  182. package/dist/core/ingestion/languages/python/query.d.ts +9 -0
  183. package/dist/core/ingestion/languages/python/query.js +267 -0
  184. package/dist/core/ingestion/languages/python/receiver-binding.d.ts +21 -0
  185. package/dist/core/ingestion/languages/python/receiver-binding.js +116 -0
  186. package/dist/core/ingestion/languages/python/scope-resolver.d.ts +16 -0
  187. package/dist/core/ingestion/languages/python/scope-resolver.js +53 -0
  188. package/dist/core/ingestion/languages/python/simple-hooks.d.ts +23 -0
  189. package/dist/core/ingestion/languages/python/simple-hooks.js +35 -0
  190. package/dist/core/ingestion/languages/python.js +14 -0
  191. package/dist/core/ingestion/model/method-registry.d.ts +9 -0
  192. package/dist/core/ingestion/model/method-registry.js +4 -0
  193. package/dist/core/ingestion/model/scope-resolution-indexes.d.ts +59 -0
  194. package/dist/core/ingestion/model/scope-resolution-indexes.js +42 -0
  195. package/dist/core/ingestion/model/semantic-model.d.ts +64 -0
  196. package/dist/core/ingestion/model/semantic-model.js +55 -0
  197. package/dist/core/ingestion/mro-processor.js +38 -22
  198. package/dist/core/ingestion/parsing-processor.d.ts +18 -1
  199. package/dist/core/ingestion/parsing-processor.js +45 -11
  200. package/dist/core/ingestion/pipeline-phases/index.d.ts +1 -0
  201. package/dist/core/ingestion/pipeline-phases/index.js +1 -0
  202. package/dist/core/ingestion/pipeline-phases/parse-impl.d.ts +10 -0
  203. package/dist/core/ingestion/pipeline-phases/parse-impl.js +17 -2
  204. package/dist/core/ingestion/pipeline-phases/parse.d.ts +18 -0
  205. package/dist/core/ingestion/pipeline.js +2 -1
  206. package/dist/core/ingestion/registry-primary-flag.d.ts +86 -0
  207. package/dist/core/ingestion/registry-primary-flag.js +111 -0
  208. package/dist/core/ingestion/resolve-references.d.ts +63 -0
  209. package/dist/core/ingestion/resolve-references.js +175 -0
  210. package/dist/core/ingestion/scope-extractor-bridge.d.ts +32 -0
  211. package/dist/core/ingestion/scope-extractor-bridge.js +44 -0
  212. package/dist/core/ingestion/scope-extractor.d.ts +86 -0
  213. package/dist/core/ingestion/scope-extractor.js +758 -0
  214. package/dist/core/ingestion/scope-resolution/contract/scope-resolver.d.ts +372 -0
  215. package/dist/core/ingestion/scope-resolution/contract/scope-resolver.js +212 -0
  216. package/dist/core/ingestion/scope-resolution/graph-bridge/edges.d.ts +43 -0
  217. package/dist/core/ingestion/scope-resolution/graph-bridge/edges.js +79 -0
  218. package/dist/core/ingestion/scope-resolution/graph-bridge/ids.d.ts +57 -0
  219. package/dist/core/ingestion/scope-resolution/graph-bridge/ids.js +112 -0
  220. package/dist/core/ingestion/scope-resolution/graph-bridge/imports-to-edges.d.ts +17 -0
  221. package/dist/core/ingestion/scope-resolution/graph-bridge/imports-to-edges.js +46 -0
  222. package/dist/core/ingestion/scope-resolution/graph-bridge/method-dispatch.d.ts +19 -0
  223. package/dist/core/ingestion/scope-resolution/graph-bridge/method-dispatch.js +30 -0
  224. package/dist/core/ingestion/scope-resolution/graph-bridge/node-lookup.d.ts +37 -0
  225. package/dist/core/ingestion/scope-resolution/graph-bridge/node-lookup.js +113 -0
  226. package/dist/core/ingestion/scope-resolution/graph-bridge/references-to-edges.d.ts +38 -0
  227. package/dist/core/ingestion/scope-resolution/graph-bridge/references-to-edges.js +73 -0
  228. package/dist/core/ingestion/scope-resolution/passes/compound-receiver.d.ts +42 -0
  229. package/dist/core/ingestion/scope-resolution/passes/compound-receiver.js +198 -0
  230. package/dist/core/ingestion/scope-resolution/passes/free-call-fallback.d.ts +27 -0
  231. package/dist/core/ingestion/scope-resolution/passes/free-call-fallback.js +131 -0
  232. package/dist/core/ingestion/scope-resolution/passes/imported-return-types.d.ts +48 -0
  233. package/dist/core/ingestion/scope-resolution/passes/imported-return-types.js +130 -0
  234. package/dist/core/ingestion/scope-resolution/passes/mro.d.ts +42 -0
  235. package/dist/core/ingestion/scope-resolution/passes/mro.js +99 -0
  236. package/dist/core/ingestion/scope-resolution/passes/overload-narrowing.d.ts +26 -0
  237. package/dist/core/ingestion/scope-resolution/passes/overload-narrowing.js +61 -0
  238. package/dist/core/ingestion/scope-resolution/passes/receiver-bound-calls.d.ts +46 -0
  239. package/dist/core/ingestion/scope-resolution/passes/receiver-bound-calls.js +327 -0
  240. package/dist/core/ingestion/scope-resolution/pipeline/phase.d.ts +47 -0
  241. package/dist/core/ingestion/scope-resolution/pipeline/phase.js +130 -0
  242. package/dist/core/ingestion/scope-resolution/pipeline/reconcile-ownership.d.ts +68 -0
  243. package/dist/core/ingestion/scope-resolution/pipeline/reconcile-ownership.js +125 -0
  244. package/dist/core/ingestion/scope-resolution/pipeline/registry.d.ts +17 -0
  245. package/dist/core/ingestion/scope-resolution/pipeline/registry.js +21 -0
  246. package/dist/core/ingestion/scope-resolution/pipeline/run.d.ts +66 -0
  247. package/dist/core/ingestion/scope-resolution/pipeline/run.js +157 -0
  248. package/dist/core/ingestion/scope-resolution/scope/namespace-targets.d.ts +36 -0
  249. package/dist/core/ingestion/scope-resolution/scope/namespace-targets.js +52 -0
  250. package/dist/core/ingestion/scope-resolution/scope/walkers.d.ts +127 -0
  251. package/dist/core/ingestion/scope-resolution/scope/walkers.js +349 -0
  252. package/dist/core/ingestion/scope-resolution/workspace-index.d.ts +52 -0
  253. package/dist/core/ingestion/scope-resolution/workspace-index.js +61 -0
  254. package/dist/core/ingestion/shadow-harness.d.ts +113 -0
  255. package/dist/core/ingestion/shadow-harness.js +148 -0
  256. package/dist/core/ingestion/utils/ast-helpers.d.ts +19 -1
  257. package/dist/core/ingestion/utils/ast-helpers.js +70 -0
  258. package/dist/core/ingestion/utils/max-file-size.d.ts +20 -0
  259. package/dist/core/ingestion/utils/max-file-size.js +52 -0
  260. package/dist/core/ingestion/workers/parse-worker.d.ts +9 -0
  261. package/dist/core/ingestion/workers/parse-worker.js +57 -21
  262. package/dist/core/lbug/lbug-adapter.d.ts +22 -2
  263. package/dist/core/lbug/lbug-adapter.js +58 -14
  264. package/dist/core/lbug/pool-adapter.d.ts +17 -0
  265. package/dist/core/lbug/pool-adapter.js +24 -14
  266. package/dist/core/run-analyze.d.ts +32 -0
  267. package/dist/core/run-analyze.js +74 -19
  268. package/dist/core/search/bm25-index.d.ts +18 -0
  269. package/dist/core/search/bm25-index.js +125 -12
  270. package/dist/core/tree-sitter/parser-loader.js +6 -1
  271. package/dist/mcp/local/local-backend.d.ts +67 -3
  272. package/dist/mcp/local/local-backend.js +296 -34
  273. package/dist/mcp/resources.d.ts +31 -0
  274. package/dist/mcp/resources.js +100 -17
  275. package/dist/mcp/tools.d.ts +4 -1
  276. package/dist/mcp/tools.js +75 -54
  277. package/dist/server/api.js +6 -2
  278. package/dist/storage/git.d.ts +49 -0
  279. package/dist/storage/git.js +111 -0
  280. package/dist/storage/repo-manager.d.ts +246 -1
  281. package/dist/storage/repo-manager.js +391 -9
  282. package/package.json +7 -6
  283. package/scripts/bench-scope-resolution.ts +134 -0
  284. package/scripts/ci-list-migrated-languages.ts +24 -0
  285. package/skills/gitnexus-cli.md +1 -0
@@ -0,0 +1,441 @@
1
+ /**
2
+ * Cross-repo impact (Phase 1 local walk + Phase 2 bridge fan-out).
3
+ * All bridge Cypher for this feature lives in this module.
4
+ */
5
+ import fsp from 'node:fs/promises';
6
+ import path from 'node:path';
7
+ import { GroupNotFoundError, loadGroupConfig } from './config-parser.js';
8
+ import { fileMatchesServicePrefix, normalizeServicePrefix, repoInSubgroup, } from './group-path-utils.js';
9
+ import { getGroupDir } from './storage.js';
10
+ import { closeBridgeDb, openBridgeDbReadOnly, queryBridge, readBridgeMeta } from './bridge-db.js';
11
+ import { BRIDGE_SCHEMA_VERSION } from './bridge-schema.js';
12
+ /** Cross-boundary hops beyond this value are clamped (multi-hop reserved for future work). */
13
+ export const MAX_SUPPORTED_CROSS_DEPTH = 1;
14
+ /** Default wall-clock budget for the Phase 1 `impact` leg when callers omit `timeoutMs`. */
15
+ export const DEFAULT_LOCAL_IMPACT_TIMEOUT_MS = 30_000;
16
+ const CY_NEIGHBORS_UPSTREAM = `
17
+ MATCH (consumer:Contract)-[l:ContractLink]->(provider:Contract)
18
+ WHERE provider.repo = $localRepo
19
+ AND provider.symbolUid IN $uids
20
+ AND provider.role = 'provider'
21
+ RETURN consumer.repo AS neighborRepo,
22
+ consumer.symbolUid AS neighborUid,
23
+ consumer.filePath AS neighborFilePath,
24
+ l.matchType AS matchType,
25
+ l.confidence AS confidence,
26
+ l.contractId AS contractId,
27
+ consumer.type AS contractType
28
+ `;
29
+ const CY_NEIGHBORS_DOWNSTREAM = `
30
+ MATCH (consumer:Contract)-[l:ContractLink]->(provider:Contract)
31
+ WHERE consumer.repo = $localRepo
32
+ AND consumer.symbolUid IN $uids
33
+ AND consumer.role = 'consumer'
34
+ RETURN provider.repo AS neighborRepo,
35
+ provider.symbolUid AS neighborUid,
36
+ provider.filePath AS neighborFilePath,
37
+ l.matchType AS matchType,
38
+ l.confidence AS confidence,
39
+ l.contractId AS contractId,
40
+ provider.type AS contractType
41
+ `;
42
+ function parseDirection(raw) {
43
+ if (raw === 'upstream' || raw === 'downstream')
44
+ return raw;
45
+ return null;
46
+ }
47
+ function clampCrossDepth(raw) {
48
+ const n = typeof raw === 'number' && Number.isFinite(raw) ? Math.floor(raw) : 1;
49
+ const d = n < 1 ? 1 : n;
50
+ if (d > MAX_SUPPORTED_CROSS_DEPTH) {
51
+ return {
52
+ depth: MAX_SUPPORTED_CROSS_DEPTH,
53
+ warning: `crossDepth was ${d}; multi-hop cross-boundary traversal beyond ${MAX_SUPPORTED_CROSS_DEPTH} is not implemented yet. Using crossDepth ${MAX_SUPPORTED_CROSS_DEPTH}.`,
54
+ };
55
+ }
56
+ return { depth: d };
57
+ }
58
+ export function validateGroupImpactParams(params) {
59
+ const name = String(params.name ?? '').trim();
60
+ const repoPath = String(params.repo ?? '').trim();
61
+ const target = String(params.target ?? '').trim();
62
+ if (!name)
63
+ return { ok: false, error: 'name is required' };
64
+ if (!repoPath)
65
+ return { ok: false, error: 'repo is required (group repo path, e.g. app/backend)' };
66
+ if (!target)
67
+ return { ok: false, error: 'target is required' };
68
+ if (params.service !== undefined &&
69
+ params.service !== null &&
70
+ String(params.service).trim() === '') {
71
+ return { ok: false, error: 'service must not be an empty string' };
72
+ }
73
+ const direction = parseDirection(params.direction);
74
+ if (!direction)
75
+ return { ok: false, error: 'direction must be upstream or downstream' };
76
+ let maxDepth = typeof params.maxDepth === 'number' && params.maxDepth > 0 ? params.maxDepth : 3;
77
+ if (maxDepth > 32)
78
+ maxDepth = 32;
79
+ const { depth: crossDepth, warning: crossDepthWarning } = clampCrossDepth(params.crossDepth);
80
+ const relationTypes = Array.isArray(params.relationTypes)
81
+ ? params.relationTypes.filter((t) => typeof t === 'string')
82
+ : undefined;
83
+ const includeTests = Boolean(params.includeTests);
84
+ let minConfidence = typeof params.minConfidence === 'number' ? params.minConfidence : 0;
85
+ if (minConfidence < 0)
86
+ minConfidence = 0;
87
+ if (minConfidence > 1)
88
+ minConfidence = 1;
89
+ const service = normalizeServicePrefix(params.service);
90
+ const subgroup = typeof params.subgroup === 'string' ? params.subgroup : undefined;
91
+ let timeoutMs = typeof params.timeoutMs === 'number' && params.timeoutMs > 0
92
+ ? params.timeoutMs
93
+ : typeof params.timeout === 'number' && params.timeout > 0
94
+ ? params.timeout
95
+ : DEFAULT_LOCAL_IMPACT_TIMEOUT_MS;
96
+ if (timeoutMs > 3_600_000)
97
+ timeoutMs = 3_600_000;
98
+ return {
99
+ ok: true,
100
+ name,
101
+ repoPath,
102
+ target,
103
+ direction,
104
+ maxDepth,
105
+ crossDepth,
106
+ crossDepthWarning,
107
+ relationTypes,
108
+ includeTests,
109
+ minConfidence,
110
+ service,
111
+ subgroup,
112
+ timeoutMs,
113
+ };
114
+ }
115
+ async function resolveGroupRepo(port, config, repoPath) {
116
+ const registryName = config.repos[repoPath];
117
+ if (!registryName) {
118
+ return { error: `Unknown repo path "${repoPath}" in this group.` };
119
+ }
120
+ try {
121
+ return await port.resolveRepo(registryName);
122
+ }
123
+ catch (e) {
124
+ return { error: e instanceof Error ? e.message : String(e) };
125
+ }
126
+ }
127
+ async function safeLocalImpact(port, repo, impactParams, timeoutMs) {
128
+ let timer;
129
+ const impactP = port.impact(repo, impactParams).catch((err) => ({
130
+ error: err instanceof Error ? err.message : String(err),
131
+ }));
132
+ const timeoutP = new Promise((resolve) => {
133
+ timer = setTimeout(() => resolve('timeout'), timeoutMs);
134
+ });
135
+ const won = await Promise.race([
136
+ impactP.then((v) => ({ tag: 'impact', v })),
137
+ timeoutP.then(() => ({ tag: 'timeout' })),
138
+ ]);
139
+ if (timer !== undefined)
140
+ clearTimeout(timer);
141
+ if (won.tag === 'timeout') {
142
+ return {
143
+ value: { error: 'Local impact timed out', partial: true },
144
+ timedOut: true,
145
+ };
146
+ }
147
+ return { value: won.v, timedOut: false };
148
+ }
149
+ export function collectImpactSymbolUids(local, servicePrefix) {
150
+ const uids = new Set();
151
+ let targetFilePath;
152
+ const obj = local;
153
+ if (!obj || typeof obj !== 'object')
154
+ return { uids: [], targetFilePath };
155
+ const target = obj.target;
156
+ if (target?.id) {
157
+ targetFilePath = typeof target.filePath === 'string' ? target.filePath : undefined;
158
+ if (fileMatchesServicePrefix(targetFilePath, servicePrefix)) {
159
+ uids.add(String(target.id));
160
+ }
161
+ }
162
+ const byDepth = obj.byDepth;
163
+ if (byDepth && typeof byDepth === 'object') {
164
+ for (const items of Object.values(byDepth)) {
165
+ if (!Array.isArray(items))
166
+ continue;
167
+ for (const it of items) {
168
+ const row = it;
169
+ if (row?.id && fileMatchesServicePrefix(row.filePath, servicePrefix)) {
170
+ uids.add(String(row.id));
171
+ }
172
+ }
173
+ }
174
+ }
175
+ return { uids: [...uids], targetFilePath };
176
+ }
177
+ function extractProcessNames(impact) {
178
+ const o = impact;
179
+ if (!o?.affected_processes)
180
+ return [];
181
+ return o.affected_processes.map((p) => String(p.name ?? '')).filter(Boolean);
182
+ }
183
+ function mergeRisk(localRisk, cross) {
184
+ const highConf = cross.some((c) => c.contract.confidence >= 0.85);
185
+ if (localRisk === 'CRITICAL')
186
+ return 'CRITICAL';
187
+ if (cross.length >= 3)
188
+ return 'CRITICAL';
189
+ if (highConf)
190
+ return 'HIGH';
191
+ if (cross.length > 0 && (localRisk === 'LOW' || localRisk === 'UNKNOWN'))
192
+ return 'MEDIUM';
193
+ return localRisk;
194
+ }
195
+ async function ensureBridgeReady(groupDir) {
196
+ const meta = await readBridgeMeta(groupDir);
197
+ if (meta.version > 0 && meta.version !== BRIDGE_SCHEMA_VERSION) {
198
+ return {
199
+ error: `Bridge schema version mismatch (meta.json has ${meta.version}, expected ${BRIDGE_SCHEMA_VERSION}). Run gitnexus group sync for this group.`,
200
+ };
201
+ }
202
+ const dbPath = path.join(groupDir, 'bridge.lbug');
203
+ try {
204
+ await fsp.access(dbPath);
205
+ }
206
+ catch {
207
+ return {
208
+ error: `No bridge.lbug in this group directory. Run gitnexus group sync (schema ${BRIDGE_SCHEMA_VERSION}).`,
209
+ };
210
+ }
211
+ const handle = await openBridgeDbReadOnly(groupDir);
212
+ if (!handle) {
213
+ return {
214
+ error: `Could not open bridge.lbug read-only (schema ${BRIDGE_SCHEMA_VERSION}). Run gitnexus group sync.`,
215
+ };
216
+ }
217
+ return { handle };
218
+ }
219
+ function rowToNeighbor(r) {
220
+ const neighborRepo = String(r.neighborRepo ?? r[0] ?? '');
221
+ const neighborUid = String(r.neighborUid ?? r[1] ?? '');
222
+ if (!neighborRepo || !neighborUid)
223
+ return null;
224
+ return {
225
+ neighborRepo,
226
+ neighborUid,
227
+ neighborFilePath: r.neighborFilePath !== undefined ? String(r.neighborFilePath) : String(r[2] ?? ''),
228
+ matchType: String(r.matchType ?? r[3] ?? 'exact'),
229
+ confidence: Number(r.confidence ?? r[4] ?? 0),
230
+ contractId: String(r.contractId ?? r[5] ?? ''),
231
+ contractType: String(r.contractType ?? r[6] ?? 'custom'),
232
+ };
233
+ }
234
+ export async function runGroupImpact(deps, params) {
235
+ const parsed = validateGroupImpactParams(params);
236
+ if (parsed.ok === false)
237
+ return { error: parsed.error };
238
+ const { name, repoPath, target, direction, maxDepth, crossDepth: _crossDepth, crossDepthWarning, relationTypes, includeTests, minConfidence, service: servicePrefix, subgroup, timeoutMs, } = parsed;
239
+ const groupDir = getGroupDir(deps.gitnexusDir, name);
240
+ let config;
241
+ try {
242
+ config = await loadGroupConfig(groupDir);
243
+ }
244
+ catch (e) {
245
+ if (e instanceof GroupNotFoundError)
246
+ return { error: `Group "${name}" not found. Run group_list to see configured groups.` };
247
+ return { error: e instanceof Error ? e.message : String(e) };
248
+ }
249
+ const resolved = await resolveGroupRepo(deps.port, config, repoPath);
250
+ if ('error' in resolved)
251
+ return { error: resolved.error };
252
+ const impactParams = {
253
+ target,
254
+ direction,
255
+ maxDepth,
256
+ relationTypes: relationTypes && relationTypes.length > 0 ? relationTypes : undefined,
257
+ includeTests,
258
+ minConfidence,
259
+ };
260
+ const deadline = Date.now() + Math.max(0, timeoutMs);
261
+ const { value: local, timedOut: localTimedOut } = await safeLocalImpact(deps.port, resolved, impactParams, timeoutMs);
262
+ if (localTimedOut) {
263
+ const _base = local;
264
+ return {
265
+ local,
266
+ group: name,
267
+ cross: [],
268
+ outOfScope: [],
269
+ truncated: true,
270
+ truncatedRepos: [],
271
+ summary: {
272
+ direct: 0,
273
+ processes_affected: 0,
274
+ modules_affected: 0,
275
+ cross_repo_hits: 0,
276
+ },
277
+ risk: 'UNKNOWN',
278
+ timeoutMs,
279
+ truncationReason: 'timeout',
280
+ crossDepthWarning,
281
+ };
282
+ }
283
+ const localObj = local;
284
+ if (localObj?.error && typeof localObj.error === 'string') {
285
+ // Fail closed: the local-impact phase errored (missing symbol, graph-load
286
+ // failure, thrown exception wrapped by safeLocalImpact, or port-returned
287
+ // `{ error }`). Do NOT wrap it into a zero-hit success payload — callers
288
+ // branch on top-level `error`, and a blast-radius tool reporting "no
289
+ // impact" on the failure path is a false negative on a safety-critical
290
+ // signal. Bubble the error so consumers treat it as a failure.
291
+ return { error: `Local impact failed for ${repoPath}: ${localObj.error}` };
292
+ }
293
+ if (servicePrefix) {
294
+ const tf = localObj?.target?.filePath;
295
+ if (!fileMatchesServicePrefix(tf, servicePrefix)) {
296
+ return {
297
+ local: {},
298
+ group: name,
299
+ cross: [],
300
+ outOfScope: [],
301
+ truncated: false,
302
+ truncatedRepos: [],
303
+ summary: {
304
+ direct: 0,
305
+ processes_affected: 0,
306
+ modules_affected: 0,
307
+ cross_repo_hits: 0,
308
+ },
309
+ risk: 'LOW',
310
+ timeoutMs,
311
+ crossDepthWarning,
312
+ };
313
+ }
314
+ }
315
+ const { uids } = collectImpactSymbolUids(local, servicePrefix);
316
+ if (uids.length === 0) {
317
+ const s = local?.summary || {};
318
+ return {
319
+ local,
320
+ group: name,
321
+ cross: [],
322
+ outOfScope: [],
323
+ truncated: Boolean(local.partial),
324
+ truncatedRepos: [],
325
+ summary: {
326
+ direct: s.direct ?? 0,
327
+ processes_affected: s.processes_affected ?? 0,
328
+ modules_affected: s.modules_affected ?? 0,
329
+ cross_repo_hits: 0,
330
+ },
331
+ risk: String(local.risk ?? 'LOW'),
332
+ timeoutMs,
333
+ truncationReason: local.partial ? 'partial' : undefined,
334
+ crossDepthWarning,
335
+ };
336
+ }
337
+ const bridgePrep = await ensureBridgeReady(groupDir);
338
+ if ('error' in bridgePrep)
339
+ return { error: bridgePrep.error };
340
+ const handle = bridgePrep.handle;
341
+ const cross = [];
342
+ const outOfScope = [];
343
+ const truncatedRepos = [];
344
+ try {
345
+ const cypher = direction === 'upstream' ? CY_NEIGHBORS_UPSTREAM : CY_NEIGHBORS_DOWNSTREAM;
346
+ const rows = await queryBridge(handle, cypher, {
347
+ localRepo: repoPath,
348
+ uids,
349
+ });
350
+ const neighbors = [];
351
+ for (const raw of rows) {
352
+ const n = rowToNeighbor(raw);
353
+ if (n)
354
+ neighbors.push(n);
355
+ }
356
+ neighbors.sort((a, b) => b.confidence - a.confidence);
357
+ const seen = new Set();
358
+ for (const n of neighbors) {
359
+ if (servicePrefix && !fileMatchesServicePrefix(n.neighborFilePath, servicePrefix)) {
360
+ continue;
361
+ }
362
+ if (!repoInSubgroup(n.neighborRepo, subgroup)) {
363
+ outOfScope.push({
364
+ from: direction === 'upstream' ? n.neighborRepo : repoPath,
365
+ to: direction === 'upstream' ? repoPath : n.neighborRepo,
366
+ contractId: n.contractId,
367
+ confidence: n.confidence,
368
+ });
369
+ continue;
370
+ }
371
+ const key = `${n.neighborRepo}\0${n.neighborUid}\0${n.contractId}`;
372
+ if (seen.has(key))
373
+ continue;
374
+ seen.add(key);
375
+ if (Date.now() > deadline) {
376
+ truncatedRepos.push(n.neighborRepo);
377
+ continue;
378
+ }
379
+ const regName = config.repos[n.neighborRepo];
380
+ if (!regName)
381
+ continue;
382
+ let neighborHandle;
383
+ try {
384
+ neighborHandle = await deps.port.resolveRepo(regName);
385
+ }
386
+ catch {
387
+ truncatedRepos.push(n.neighborRepo);
388
+ continue;
389
+ }
390
+ const fan = await deps.port.impactByUid(neighborHandle.id, n.neighborUid, direction, {
391
+ maxDepth,
392
+ relationTypes: relationTypes ?? [],
393
+ minConfidence,
394
+ includeTests,
395
+ });
396
+ if (fan == null) {
397
+ truncatedRepos.push(n.neighborRepo);
398
+ continue;
399
+ }
400
+ cross.push({
401
+ repo: regName,
402
+ repo_path: n.neighborRepo,
403
+ contract: {
404
+ id: n.contractId,
405
+ type: n.contractType,
406
+ match_type: n.matchType || 'exact',
407
+ confidence: n.confidence,
408
+ },
409
+ by_depth: (fan.byDepth ?? {}),
410
+ affected_processes: extractProcessNames(fan),
411
+ });
412
+ }
413
+ }
414
+ finally {
415
+ await closeBridgeDb(handle);
416
+ }
417
+ const localSum = local?.summary || {};
418
+ const localRisk = String(local.risk ?? 'LOW');
419
+ const localPartial = Boolean(local.partial);
420
+ const truncated = truncatedRepos.length > 0 || localPartial;
421
+ const result = {
422
+ local,
423
+ group: name,
424
+ cross,
425
+ outOfScope,
426
+ truncated,
427
+ truncatedRepos: [...new Set(truncatedRepos)],
428
+ summary: {
429
+ direct: localSum.direct ?? 0,
430
+ processes_affected: localSum.processes_affected ?? 0,
431
+ modules_affected: localSum.modules_affected ?? 0,
432
+ cross_repo_hits: cross.length,
433
+ },
434
+ risk: mergeRisk(localRisk, cross),
435
+ timeoutMs,
436
+ truncationReason: truncated ? 'partial' : undefined,
437
+ crossDepthWarning,
438
+ };
439
+ return result;
440
+ }
441
+ export { normalizeServicePrefix, fileMatchesServicePrefix } from './group-path-utils.js';
@@ -1,27 +1,71 @@
1
1
  import PHP from 'tree-sitter-php';
2
2
  import { compilePatterns, runCompiledPatterns, unquoteLiteral, } from '../tree-sitter-scanner.js';
3
3
  /**
4
- * PHP HTTP plugin — Laravel `Route::get/post/...` declarations.
4
+ * PHP HTTP plugin.
5
+ *
6
+ * Providers:
7
+ * - Laravel `Route::get/post/...`
8
+ *
9
+ * Consumers (string-literal URLs only):
10
+ * - Laravel HTTP client: `Http::get/post/put/delete/patch($url)`
11
+ * - Guzzle / generic object method: `$client->get/post/...($url)`
12
+ * - `file_get_contents($url)`
5
13
  *
6
14
  * The pipeline already uses `PHP.php_only` for ingesting plain `.php`
7
15
  * files (see `core/tree-sitter/parser-loader.ts`), and we do the same
8
16
  * here so Laravel route files are parsed with the right grammar dialect.
17
+ *
18
+ * Scope notes: consumer patterns match string literals only. URLs built
19
+ * via binary concatenation (`$base . '/path'`), `sprintf`, or config
20
+ * lookup (`config('services.foo.base').'/path'`) are intentionally left
21
+ * for a follow-up — they require constant-folding the surrounding
22
+ * scope to be meaningful.
9
23
  */
10
- const LARAVEL_PATTERNS = compilePatterns({
11
- name: 'php-laravel',
24
+ const LARAVEL_ROUTE_SPEC = {
25
+ meta: {},
26
+ query: `
27
+ (scoped_call_expression
28
+ scope: (name) @scope (#eq? @scope "Route")
29
+ name: (name) @method (#match? @method "^(get|post|put|delete|patch)$")
30
+ arguments: (arguments . (argument (string) @path)))
31
+ `,
32
+ };
33
+ const HTTP_FACADE_SPEC = {
34
+ meta: {},
35
+ query: `
36
+ (scoped_call_expression
37
+ scope: (name) @scope (#eq? @scope "Http")
38
+ name: (name) @method (#match? @method "^(get|post|put|delete|patch)$")
39
+ arguments: (arguments . (argument (string) @path)))
40
+ `,
41
+ };
42
+ const GUZZLE_MEMBER_SPEC = {
43
+ meta: {},
44
+ query: `
45
+ (member_call_expression
46
+ name: (name) @method (#match? @method "^(get|post|put|delete|patch)$")
47
+ arguments: (arguments . (argument (string) @path)))
48
+ `,
49
+ };
50
+ const FILE_GET_CONTENTS_SPEC = {
51
+ meta: {},
52
+ query: `
53
+ (function_call_expression
54
+ function: (name) @fn (#eq? @fn "file_get_contents")
55
+ arguments: (arguments . (argument (string) @path)))
56
+ `,
57
+ };
58
+ const mk = (spec, suffix) => compilePatterns({
59
+ name: `php-${suffix}`,
12
60
  language: PHP.php_only,
13
- patterns: [
14
- {
15
- meta: {},
16
- query: `
17
- (scoped_call_expression
18
- scope: (name) @scope (#eq? @scope "Route")
19
- name: (name) @method (#match? @method "^(get|post|put|delete|patch)$")
20
- arguments: (arguments . (argument (string) @path)))
21
- `,
22
- },
23
- ],
61
+ patterns: [spec],
24
62
  });
63
+ const PHP_PATTERNS = {
64
+ laravelRoute: mk(LARAVEL_ROUTE_SPEC, 'laravel-route'),
65
+ httpFacade: mk(HTTP_FACADE_SPEC, 'http-facade'),
66
+ guzzleMember: mk(GUZZLE_MEMBER_SPEC, 'guzzle-member'),
67
+ fileGetContents: mk(FILE_GET_CONTENTS_SPEC, 'file-get-contents'),
68
+ };
25
69
  /**
26
70
  * Extract the inner text of a PHP `string` node. The tree-sitter-php
27
71
  * grammar wraps single / double-quoted literals differently depending
@@ -30,12 +74,9 @@ const LARAVEL_PATTERNS = compilePatterns({
30
74
  * child nodes.
31
75
  */
32
76
  function phpStringText(node) {
33
- // Most single-quoted strings expose their inner content through the
34
- // full node text (including quotes), which unquoteLiteral strips.
35
77
  const direct = unquoteLiteral(node.text);
36
78
  if (direct !== null && direct !== node.text)
37
79
  return direct;
38
- // Fall back to child string_content / string_value node if present.
39
80
  for (const child of node.children) {
40
81
  if (child.type === 'string_content' || child.type === 'string_value') {
41
82
  return child.text;
@@ -43,12 +84,29 @@ function phpStringText(node) {
43
84
  }
44
85
  return direct;
45
86
  }
87
+ /**
88
+ * HTTP client helpers (`Http::`, Guzzle) are almost always called with
89
+ * a path relative to a configured base URL, or a full URL. File paths
90
+ * are rare. Accept both relative (`/api/...`) and absolute (`http(s)://`).
91
+ */
92
+ function isHttpClientPath(path) {
93
+ return path.startsWith('/') || path.startsWith('http://') || path.startsWith('https://');
94
+ }
95
+ /**
96
+ * `file_get_contents` is used for both HTTP and filesystem reads. Only
97
+ * emit a consumer contract when the URL is an absolute HTTP(S) URL to
98
+ * avoid false positives for local file paths and stream wrappers
99
+ * (`php://input`, `file://`, `data:`, ...).
100
+ */
101
+ function isHttpUrlLiteral(path) {
102
+ return path.startsWith('http://') || path.startsWith('https://');
103
+ }
46
104
  export const PHP_HTTP_PLUGIN = {
47
105
  name: 'php-http',
48
106
  language: PHP.php_only,
49
107
  scan(tree) {
50
108
  const out = [];
51
- for (const match of runCompiledPatterns(LARAVEL_PATTERNS, tree)) {
109
+ for (const match of runCompiledPatterns(PHP_PATTERNS.laravelRoute, tree)) {
52
110
  const methodNode = match.captures.method;
53
111
  const pathNode = match.captures.path;
54
112
  if (!methodNode || !pathNode)
@@ -65,6 +123,56 @@ export const PHP_HTTP_PLUGIN = {
65
123
  confidence: 0.8,
66
124
  });
67
125
  }
126
+ for (const match of runCompiledPatterns(PHP_PATTERNS.httpFacade, tree)) {
127
+ const methodNode = match.captures.method;
128
+ const pathNode = match.captures.path;
129
+ if (!methodNode || !pathNode)
130
+ continue;
131
+ const path = phpStringText(pathNode);
132
+ if (path === null || !isHttpClientPath(path))
133
+ continue;
134
+ out.push({
135
+ role: 'consumer',
136
+ framework: 'laravel-http',
137
+ method: methodNode.text.toUpperCase(),
138
+ path,
139
+ name: null,
140
+ confidence: 0.7,
141
+ });
142
+ }
143
+ for (const match of runCompiledPatterns(PHP_PATTERNS.guzzleMember, tree)) {
144
+ const methodNode = match.captures.method;
145
+ const pathNode = match.captures.path;
146
+ if (!methodNode || !pathNode)
147
+ continue;
148
+ const path = phpStringText(pathNode);
149
+ if (path === null || !isHttpClientPath(path))
150
+ continue;
151
+ out.push({
152
+ role: 'consumer',
153
+ framework: 'guzzle',
154
+ method: methodNode.text.toUpperCase(),
155
+ path,
156
+ name: null,
157
+ confidence: 0.7,
158
+ });
159
+ }
160
+ for (const match of runCompiledPatterns(PHP_PATTERNS.fileGetContents, tree)) {
161
+ const pathNode = match.captures.path;
162
+ if (!pathNode)
163
+ continue;
164
+ const path = phpStringText(pathNode);
165
+ if (path === null || !isHttpUrlLiteral(path))
166
+ continue;
167
+ out.push({
168
+ role: 'consumer',
169
+ framework: 'file-get-contents',
170
+ method: 'GET',
171
+ path,
172
+ name: null,
173
+ confidence: 0.7,
174
+ });
175
+ }
68
176
  return out;
69
177
  },
70
178
  };
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Shared service-path normalization for group tools (`service` monorepo filter)
3
+ * and subgroup membership checks.
4
+ *
5
+ * Inputs may originate from tree-sitter, the OS file API, or user-supplied
6
+ * MCP arguments, so both `\` and `/` separators are accepted. Internally we
7
+ * normalize to POSIX-style `/` for case-sensitive segment comparisons.
8
+ */
9
+ export declare function normalizeServicePrefix(service: unknown): string | undefined;
10
+ export declare function fileMatchesServicePrefix(filePath: string | undefined, prefix: string | undefined): boolean;
11
+ /**
12
+ * True if `repoPath` is at or beneath `subgroup` (member-path prefix in
13
+ * `group.yaml`). Empty / missing `subgroup` matches every repo.
14
+ *
15
+ * @param exact When set, requires an exact equality match (no descendant repos).
16
+ */
17
+ export declare function repoInSubgroup(repoPath: string, subgroup?: string, exact?: boolean): boolean;
@@ -0,0 +1,40 @@
1
+ /**
2
+ * Shared service-path normalization for group tools (`service` monorepo filter)
3
+ * and subgroup membership checks.
4
+ *
5
+ * Inputs may originate from tree-sitter, the OS file API, or user-supplied
6
+ * MCP arguments, so both `\` and `/` separators are accepted. Internally we
7
+ * normalize to POSIX-style `/` for case-sensitive segment comparisons.
8
+ */
9
+ function toPosix(p) {
10
+ return p.replace(/\\/g, '/');
11
+ }
12
+ export function normalizeServicePrefix(service) {
13
+ if (service === undefined || service === null)
14
+ return undefined;
15
+ const s = toPosix(String(service)).trim().replace(/\/+$/, '');
16
+ return s.length > 0 ? s : undefined;
17
+ }
18
+ export function fileMatchesServicePrefix(filePath, prefix) {
19
+ if (!prefix)
20
+ return true;
21
+ if (!filePath)
22
+ return false;
23
+ const normalized = toPosix(filePath);
24
+ return normalized === prefix || normalized.startsWith(`${prefix}/`);
25
+ }
26
+ /**
27
+ * True if `repoPath` is at or beneath `subgroup` (member-path prefix in
28
+ * `group.yaml`). Empty / missing `subgroup` matches every repo.
29
+ *
30
+ * @param exact When set, requires an exact equality match (no descendant repos).
31
+ */
32
+ export function repoInSubgroup(repoPath, subgroup, exact) {
33
+ if (!subgroup?.trim())
34
+ return true;
35
+ const s = toPosix(subgroup).replace(/\/+$/, '');
36
+ const r = toPosix(repoPath);
37
+ if (exact)
38
+ return r === s;
39
+ return r === s || r.startsWith(`${s}/`);
40
+ }
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Map MCP/CLI `@groupName` or `@groupName/memberPath` to a concrete member path in group.yaml.
3
+ */
4
+ export declare function resolveAtGroupMemberRepoPath(groupName: string, explicitMemberPath: string | undefined): Promise<{
5
+ ok: true;
6
+ repoPath: string;
7
+ } | {
8
+ ok: false;
9
+ error: string;
10
+ }>;