rm(list = ls())
seed <- 1
set.seed(seed)
require(kaiaulu)
require(igraph)
require(visNetwork)
require(data.table)
require(yaml)
require(stringi)
require(knitr)

1 Introduction

This notebook demonstrates how to compute radio silence — a social smell that identifies developers who act as sole bridges between otherwise disconnected communication communities. The pipeline uses GitHub issue and pull request reply data.

Radio silence boundary spanners are authors who are the only link between two distinct developer communities. If they stop communicating, the communities lose contact entirely, creating a coordination risk.

The pipeline proceeds as follows:

  1. Parse GitHub issue and pull request replies into a reply table
  2. Transform into a bipartite author-thread network
  3. Project to a unimodal author-author network
  4. Detect communities with OSLOM
  5. Count radio silence boundary spanners with smell_radio_silence

2 Project Configuration

tool <- parse_config("../tools.yml")
conf <- parse_config("../conf/kaiaulu.yml")

oslom_undir_path           <- get_tool_project("oslom_undir", tool)
dv8_path                   <- get_tool_project("dv8", tool)

github_issue_path          <- get_github_issue_path(conf, "project_key_1")
github_pull_request_path   <- get_github_pull_request_path(conf, "project_key_1")
github_reply_path          <- get_github_issue_or_pr_comment_path(conf, "project_key_1")
github_commit_path         <- get_github_commit_path(conf, "project_key_1")
github_pr_comments_path    <- get_github_pr_comments_path(conf, "project_key_1")

# DV8 parameters
project_path <- get_dv8_folder_path(conf)
project_name <- stringi::stri_split_regex(project_path, pattern = "/")[[1]]
project_name <- project_name[length(project_name)]

3 Parse GitHub Replies

Parse all GitHub issues, pull requests, and comments into a single reply table.

project_github_replies <- parse_github_replies(
  issues_json_folder_path        = github_issue_path,
  pull_requests_json_folder_path = github_pull_request_path,
  comments_json_folder_path      = github_reply_path,
  commit_json_folder_path        = github_commit_path,
  pr_comments_json_folder_path   = github_pr_comments_path
)
nrow(project_github_replies)
## [1] 1648
kable(head(project_github_replies[, .(reply_from, reply_subject)]))
reply_from reply_subject
BenjyNStrauss 306
BenjyNStrauss 306
BenjyNStrauss 306
BenjyNStrauss 306
BenjyNStrauss 306
CorneJB 128

4 Build Bipartite Author-Thread Network

Transform the reply table into a bipartite graph where one node type represents authors and the other represents issue/PR threads.

reply_network <- transform_reply_to_bipartite_network(project_github_replies)
kable(head(reply_network[["nodes"]]))
name type color
BenjyNStrauss TRUE black
CorneJB TRUE black
MahsaBazzaz TRUE black
Michelle4929 TRUE black
RavenMarQ TRUE black
Ruben Jacobo TRUE black
kable(head(reply_network[["edgelist"]]))
from to weight direction
BenjyNStrauss 306 5 directed
CorneJB 128 1 directed
CorneJB 101 2 directed
CorneJB 98 2 directed
CorneJB 96 1 directed
CorneJB 95 2 directed

5 Project to Author-Author Network

Project the bipartite graph onto the author node type to obtain a unimodal author-author co-communication network. An edge between two authors indicates they participated in the same thread.

author_network <- bipartite_graph_projection(reply_network, mode = TRUE,
                                             weight_scheme_function = weight_scheme_sum_edges)
kable(head(author_network[["nodes"]]))
name type color
BenjyNStrauss TRUE black
CorneJB TRUE black
MahsaBazzaz TRUE black
Michelle4929 TRUE black
RavenMarQ TRUE black
Ruben Jacobo TRUE black
kable(head(author_network[["edgelist"]]))
from to weight direction
BenjyNStrauss Carlos Paradis 9 undirected
Michelle4929 codecov[bot] 4 undirected
Michelle4929 Carlos Paradis 129 undirected
Carlos Paradis cohenruport 56 undirected
Michelle4929 cohenruport 21 undirected
Carlos Paradis cfuke1 58 undirected

5.1 Visualize Author-Author Network

is_directed <- any(author_network[["edgelist"]][["direction"]] == "directed")
i_author_graph <- igraph::graph_from_data_frame(
  d        = author_network[["edgelist"]],
  directed = is_directed,
  vertices = author_network[["nodes"]]
)
visIgraph(i_author_graph, randomSeed = seed)

6 Community Detection

Detect communication communities using OSLOM. Each cluster groups authors who communicate densely with one another.

mail_clusters <- community_oslom(oslom_undir_path, author_network, seed = seed, n_runs = 1000)
kable(mail_clusters[["assignment"]])
node_id cluster_id
BenjyNStrauss 1
Carlos Paradis 2
Michelle4929 3
codecov[bot] 4
cohenruport 5
cfuke1 6
john-a-flinn 7
rnkazman 8
RavenMarQ 9
beydlern 10
Mark Burgess 11
daomcgill 12
Sean Sunoo 13
connorn-dev 14
geraldmjhuff 15
Anthony Lau 16
usradam 17
Ian Jaymes Iwata 18
nicolehoess 19
ryanseng03 20
haotian1028 21
lh-zhan 22
splimon 23
jseto808 24
CorneJB 25
MahsaBazzaz 26
Ruben Jacobo 27
harrismumtaz 28
Leilani Reich 29
malialiu 30
massihonda 31
mumtaz-haris 32
Nico 33
tuejari 34
valentina-lenarduzzi 35
Waylon Ho 36
kable(mail_clusters[["info"]])
cluster_id cluster_size cluster_pvalue
1 1 1
2 1 1
3 1 1
4 1 1
5 1 1
6 1 1
7 1 1
8 1 1
9 1 1
10 1 1
11 1 1
12 1 1
13 1 1
14 1 1
15 1 1
16 1 1
17 1 1
18 1 1
19 1 1
20 1 1
21 1 1
22 1 1
23 1 1
24 1 1
25 1 NA
26 1 NA
27 1 NA
28 1 NA
29 1 NA
30 1 NA
31 1 NA
32 1 NA
33 1 NA
34 1 NA
35 1 NA
36 1 NA

7 Radio Silence

Count the number of boundary spanners — authors who are the sole bridge between otherwise disconnected communities. An output of 0 means all communities are either well-connected or isolated with no single bottleneck author.

brokers <- smell_radio_silence(mail.graph = author_network, clusters = mail_clusters)
cat("Number of radio silence boundary spanners:", length(brokers), "\n")
## Number of radio silence boundary spanners: 36
if (length(brokers) > 0) {
  kable(data.table(boundary_spanner = brokers))
}
boundary_spanner
BenjyNStrauss
Carlos Paradis
Michelle4929
codecov[bot]
cohenruport
cfuke1
john-a-flinn
rnkazman
RavenMarQ
beydlern
Mark Burgess
daomcgill
Sean Sunoo
connorn-dev
geraldmjhuff
Anthony Lau
usradam
Ian Jaymes Iwata
nicolehoess
ryanseng03
haotian1028
lh-zhan
splimon
jseto808
CorneJB
MahsaBazzaz
Ruben Jacobo
harrismumtaz
Leilani Reich
malialiu
massihonda
mumtaz-haris
Nico
tuejari
valentina-lenarduzzi
Waylon Ho

8 Radio Silence via DV8 DR Space Clustering

This section repeats the pipeline above but replaces OSLOM community detection with DV8 DR Space hierarchical clustering. The author-author network is exported as a DSM, clustered by DR Space, and radio silence is computed per layer using module assignments.

8.1 Export DSM and Run DR Space

dir.create(path.expand(project_path), showWarnings = FALSE, recursive = TRUE)

adsmj_path <- file.path(project_path, paste0(project_name, "-author-hdsm.json"))
graph_to_dsmj(author_network, dsmj_path = adsmj_path,
              dsmj_name = paste0(project_name, "-author-hdsm"))
## [1] "../../analysis/junit/dv8//-author-hdsm.json"
adsmb_path <- file.path(project_path, paste0(project_name, "-author-hdsm.dv8-dsm"))
dv8_dsmj_to_dsmb(dv8_path = dv8_path, dsmj_path = adsmj_path, dsmb_path = adsmb_path)
## [1] "../../analysis/junit/dv8//-author-hdsm.dv8-dsm"
clsx_path <- file.path(project_path, paste0(project_name, "-author-clsx.dv8-clsx"))
dv8_mdsmb_to_hierclsxb(dv8_path = dv8_path, mdsmb_path = adsmb_path,
                        hierclsxb_path = clsx_path, recursive = TRUE)
## [1] "../../analysis/junit/dv8//-author-clsx.dv8-clsx"
clsxj_path <- file.path(project_path, paste0(project_name, "-author-clsx.json"))
dv8_clsxb_to_clsxj(dv8_path = dv8_path, clsxb_path = clsx_path, clsxj_path = clsxj_path)
## [1] "../../analysis/junit/dv8//-author-clsx.json"
cluster_table <- parse_dv8_clusters(clsxj_path)
kable(cluster_table[, .N, by = .(layer, module)][order(layer, module)])
layer module N
Control Isolated 1
Isolated Isolated 12
L0 M0 19
L0 M1 1
L0 M2 1
L0 M3 1
L0 M4 1
L0/M0 Control 1
L0/M0 L0 18
L0/M0/Control Isolated 1
L0/M0/L0 M0 13
L0/M0/L0 M1 5
L0/M0/L0/M0 Control 2
L0/M0/L0/M0 L0 11
L0/M0/L0/M0/Control M1 1
L0/M0/L0/M0/Control M2 1
L0/M0/L0/M0/Control/M1 Isolated 1
L0/M0/L0/M0/Control/M2 Isolated 1
L0/M0/L0/M0/L0 M0 10
L0/M0/L0/M0/L0 M1 1
L0/M0/L0/M0/L0/M0 Control 1
L0/M0/L0/M0/L0/M0 L0 9
L0/M0/L0/M0/L0/M0/Control Isolated 1
L0/M0/L0/M0/L0/M0/L0 M0 4
L0/M0/L0/M0/L0/M0/L0 M1 5
L0/M0/L0/M0/L0/M0/L0/M0 Control 1
L0/M0/L0/M0/L0/M0/L0/M0 L0 3
L0/M0/L0/M0/L0/M0/L0/M0/Control Isolated 1
L0/M0/L0/M0/L0/M0/L0/M0/L0 M0 1
L0/M0/L0/M0/L0/M0/L0/M0/L0 M1 2
L0/M0/L0/M0/L0/M0/L0/M0/L0/M0 Isolated 1
L0/M0/L0/M0/L0/M0/L0/M0/L0/M1 Isolated 2
L0/M0/L0/M0/L0/M0/L0/M1 Control 1
L0/M0/L0/M0/L0/M0/L0/M1 L0 4
L0/M0/L0/M0/L0/M0/L0/M1/Control Isolated 1
L0/M0/L0/M0/L0/M0/L0/M1/L0 M0 1
L0/M0/L0/M0/L0/M0/L0/M1/L0 M1 3
L0/M0/L0/M0/L0/M0/L0/M1/L0/M0 Isolated 1
L0/M0/L0/M0/L0/M0/L0/M1/L0/M1 Control 1
L0/M0/L0/M0/L0/M0/L0/M1/L0/M1 L0 2
L0/M0/L0/M0/L0/M0/L0/M1/L0/M1/Control Isolated 1
L0/M0/L0/M0/L0/M0/L0/M1/L0/M1/L0 M0 1
L0/M0/L0/M0/L0/M0/L0/M1/L0/M1/L0 M1 1
L0/M0/L0/M0/L0/M0/L0/M1/L0/M1/L0/M0 Isolated 1
L0/M0/L0/M0/L0/M0/L0/M1/L0/M1/L0/M1 Isolated 1
L0/M0/L0/M0/L0/M1 Isolated 1
L0/M0/L0/M1 Control 1
L0/M0/L0/M1 L0 4
L0/M0/L0/M1/Control Isolated 1
L0/M0/L0/M1/L0 M0 1
L0/M0/L0/M1/L0 M1 1
L0/M0/L0/M1/L0 M2 2
L0/M0/L0/M1/L0/M0 Isolated 1
L0/M0/L0/M1/L0/M1 Isolated 1
L0/M0/L0/M1/L0/M2 Isolated 2
L0/M1 Isolated 1
L0/M2 Isolated 1
L0/M3 Isolated 1
L0/M4 Isolated 1

8.2 Radio Silence per Layer

For each layer in the DR Space hierarchy, call radio silence using the module assignments as communities. Layers with fewer than two distinct non-Isolated modules are skipped.

all_brokers <- character(0)

for (lyr in unique(cluster_table$layer)) {
  layer_dt       <- cluster_table[layer == lyr]
  active_modules <- setdiff(unique(layer_dt$module), "Isolated")
  if (length(active_modules) < 2) next

  cat("\n### Layer:", lyr, "\n")

  module_levels <- unique(layer_dt$module)
  module_to_id  <- seq_along(module_levels)
  names(module_to_id) <- module_levels

  assignment <- data.table(node_id    = layer_dt$file_path,
                           cluster_id = as.character(module_to_id[layer_dt$module]))
  info_dt    <- assignment[, .(cluster_size = .N), by = cluster_id]
  layer_clusters <- list(assignment = assignment, info = info_dt)

  layer_nodes    <- layer_dt$file_path
  layer_network  <- author_network
  layer_network[["nodes"]]    <- author_network[["nodes"]][author_network[["nodes"]]$name %in% layer_nodes, ]
  layer_network[["edgelist"]] <- author_network[["edgelist"]][author_network[["edgelist"]]$from %in% layer_nodes &
                                                                author_network[["edgelist"]]$to   %in% layer_nodes, ]

  brokers <- smell_radio_silence(mail.graph = layer_network, clusters = layer_clusters)
  cat("Radio silence boundary spanners:", length(brokers), "\n")
  if (length(brokers) > 0) {
    print(kable(data.table(layer = lyr, boundary_spanner = brokers)))
    all_brokers <- union(all_brokers, brokers)
  }
}
## 
## ### Layer: L0 
## Radio silence boundary spanners: 4 
## 
## 
## |layer |boundary_spanner                                          |
## |:-----|:---------------------------------------------------------|
## |L0    |ryanseng03                                                |
## |L0    |BenjyNStrauss                                             |
## |L0    |lh-zhan                                                   |
## |L0    |nicolehoess 86601771+nicolehoess@users.noreply.github.com |
## 
## ### Layer: L0/M0 
## Radio silence boundary spanners: 1 
## 
## 
## |layer |boundary_spanner |
## |:-----|:----------------|
## |L0/M0 |codecov[bot]     |
## 
## ### Layer: L0/M0/L0 
## Radio silence boundary spanners: 0 
## 
## ### Layer: L0/M0/L0/M0 
## Radio silence boundary spanners: 0 
## 
## ### Layer: L0/M0/L0/M0/L0 
## Radio silence boundary spanners: 1 
## 
## 
## |layer          |boundary_spanner                                                |
## |:--------------|:---------------------------------------------------------------|
## |L0/M0/L0/M0/L0 |Ian Jaymes Iwata 97856957+ian-lastname@users.noreply.github.com |
## 
## ### Layer: L0/M0/L0/M0/L0/M0 
## Radio silence boundary spanners: 1 
## 
## 
## |layer             |boundary_spanner |
## |:-----------------|:----------------|
## |L0/M0/L0/M0/L0/M0 |geraldmjhuff     |
## 
## ### Layer: L0/M0/L0/M0/L0/M0/L0 
## Radio silence boundary spanners: 0 
## 
## ### Layer: L0/M0/L0/M0/L0/M0/L0/M0 
## Radio silence boundary spanners: 1 
## 
## 
## |layer                   |boundary_spanner |
## |:-----------------------|:----------------|
## |L0/M0/L0/M0/L0/M0/L0/M0 |usradam          |
## 
## ### Layer: L0/M0/L0/M0/L0/M0/L0/M0/L0 
## Radio silence boundary spanners: 1 
## 
## 
## |layer                      |boundary_spanner |
## |:--------------------------|:----------------|
## |L0/M0/L0/M0/L0/M0/L0/M0/L0 |haotian1028      |
## 
## ### Layer: L0/M0/L0/M0/L0/M0/L0/M1 
## Radio silence boundary spanners: 1 
## 
## 
## |layer                   |boundary_spanner |
## |:-----------------------|:----------------|
## |L0/M0/L0/M0/L0/M0/L0/M1 |beydlern         |
## 
## ### Layer: L0/M0/L0/M0/L0/M0/L0/M1/L0 
## Radio silence boundary spanners: 1 
## 
## 
## |layer                      |boundary_spanner                                     |
## |:--------------------------|:----------------------------------------------------|
## |L0/M0/L0/M0/L0/M0/L0/M1/L0 |Sean Sunoo 97641529+Ssunoo2@users.noreply.github.com |
## 
## ### Layer: L0/M0/L0/M0/L0/M0/L0/M1/L0/M1 
## Radio silence boundary spanners: 1 
## 
## 
## |layer                         |boundary_spanner                |
## |:-----------------------------|:-------------------------------|
## |L0/M0/L0/M0/L0/M0/L0/M1/L0/M1 |Mark Burgess mkh.burg@gmail.com |
## 
## ### Layer: L0/M0/L0/M0/L0/M0/L0/M1/L0/M1/L0 
## Radio silence boundary spanners: 2 
## 
## 
## |layer                            |boundary_spanner                                          |
## |:--------------------------------|:---------------------------------------------------------|
## |L0/M0/L0/M0/L0/M0/L0/M1/L0/M1/L0 |RavenMarQ 143663502+RavenMarQ@users.noreply.github.com    |
## |L0/M0/L0/M0/L0/M0/L0/M1/L0/M1/L0 |Anthony Lau 98019016+anthonyjlau@users.noreply.github.com |
## 
## ### Layer: L0/M0/L0/M0/Control 
## Radio silence boundary spanners: 2 
## 
## 
## |layer               |boundary_spanner |
## |:-------------------|:----------------|
## |L0/M0/L0/M0/Control |daomcgill        |
## |L0/M0/L0/M0/Control |connorn-dev      |
## 
## ### Layer: L0/M0/L0/M1 
## Radio silence boundary spanners: 1 
## 
## 
## |layer       |boundary_spanner |
## |:-----------|:----------------|
## |L0/M0/L0/M1 |Michelle4929     |
## 
## ### Layer: L0/M0/L0/M1/L0 
## Radio silence boundary spanners: 2 
## 
## 
## |layer          |boundary_spanner |
## |:--------------|:----------------|
## |L0/M0/L0/M1/L0 |cohenruport      |
## |L0/M0/L0/M1/L0 |cfuke1           |

8.3 Boundary Spanner Neighborhood

Visualize the 1-hop neighborhood of all boundary spanners in the full author-author network. Each spanner node and its direct connections are shown.

htmltools::tagList(lapply(all_brokers, function(broker) {
  broker_edges <- author_network[["edgelist"]][
    author_network[["edgelist"]]$from == broker |
    author_network[["edgelist"]]$to   == broker, ]
  broker_nodes   <- unique(c(broker_edges$from, broker_edges$to))
  broker_node_dt <- author_network[["nodes"]][author_network[["nodes"]]$name %in% broker_nodes, ]
  igraph::graph_from_data_frame(
    d        = broker_edges,
    directed = FALSE,
    vertices = broker_node_dt
  ) |> visIgraph(randomSeed = seed)
}))