diff --git a/sharness/__snapshots__/example-github-load/data/sourcecred/example-github/git/graph.json b/sharness/__snapshots__/example-github-load/data/sourcecred/example-github/git/graph.json index 19a5e79..e5a5a7f 100644 --- a/sharness/__snapshots__/example-github-load/data/sourcecred/example-github/git/graph.json +++ b/sharness/__snapshots__/example-github-load/data/sourcecred/example-github/git/graph.json @@ -1 +1 @@ -[{"type":"sourcecred/graph","version":"0.4.0"},{"edges":[{"address":["sourcecred","git","HAS_PARENT","2","COMMIT","0a223346b4e6dec0127b1e6aa892c4ee0424b66a","2","COMMIT","ec91adb718a6045b492303f00d8e8beb957dc780"],"dstIndex":4,"srcIndex":0},{"address":["sourcecred","git","HAS_PARENT","2","COMMIT","0a223346b4e6dec0127b1e6aa892c4ee0424b66a","2","COMMIT","ecc889dc94cf6da17ae6eab5bb7b7155f577519d"],"dstIndex":5,"srcIndex":0},{"address":["sourcecred","git","HAS_PARENT","2","COMMIT","6bd1b4c0b719c22c688a74863be07a699b7b9b34","2","COMMIT","c430bd74455105f77215ece51945094ceeee6c86"],"dstIndex":3,"srcIndex":1},{"address":["sourcecred","git","HAS_PARENT","2","COMMIT","6d5b3aa31ebb68a06ceb46bbd6cf49b6ccd6f5e6","2","COMMIT","0a223346b4e6dec0127b1e6aa892c4ee0424b66a"],"dstIndex":0,"srcIndex":2},{"address":["sourcecred","git","HAS_PARENT","2","COMMIT","c430bd74455105f77215ece51945094ceeee6c86","2","COMMIT","6d5b3aa31ebb68a06ceb46bbd6cf49b6ccd6f5e6"],"dstIndex":2,"srcIndex":3},{"address":["sourcecred","git","HAS_PARENT","2","COMMIT","ecc889dc94cf6da17ae6eab5bb7b7155f577519d","2","COMMIT","ec91adb718a6045b492303f00d8e8beb957dc780"],"dstIndex":4,"srcIndex":5}],"nodes":[["sourcecred","git","COMMIT","0a223346b4e6dec0127b1e6aa892c4ee0424b66a"],["sourcecred","git","COMMIT","6bd1b4c0b719c22c688a74863be07a699b7b9b34"],["sourcecred","git","COMMIT","6d5b3aa31ebb68a06ceb46bbd6cf49b6ccd6f5e6"],["sourcecred","git","COMMIT","c430bd74455105f77215ece51945094ceeee6c86"],["sourcecred","git","COMMIT","ec91adb718a6045b492303f00d8e8beb957dc780"],["sourcecred","git","COMMIT","ecc889dc94cf6da17ae6eab5bb7b7155f577519d"]]}] \ No newline at end of file +[{"type":"sourcecred/graph","version":"0.5.0"},{"edges":[{"address":["sourcecred","git","HAS_PARENT","2","COMMIT","0a223346b4e6dec0127b1e6aa892c4ee0424b66a","2","COMMIT","ec91adb718a6045b492303f00d8e8beb957dc780"],"dstIndex":4,"srcIndex":0},{"address":["sourcecred","git","HAS_PARENT","2","COMMIT","0a223346b4e6dec0127b1e6aa892c4ee0424b66a","2","COMMIT","ecc889dc94cf6da17ae6eab5bb7b7155f577519d"],"dstIndex":5,"srcIndex":0},{"address":["sourcecred","git","HAS_PARENT","2","COMMIT","6bd1b4c0b719c22c688a74863be07a699b7b9b34","2","COMMIT","c430bd74455105f77215ece51945094ceeee6c86"],"dstIndex":3,"srcIndex":1},{"address":["sourcecred","git","HAS_PARENT","2","COMMIT","6d5b3aa31ebb68a06ceb46bbd6cf49b6ccd6f5e6","2","COMMIT","0a223346b4e6dec0127b1e6aa892c4ee0424b66a"],"dstIndex":0,"srcIndex":2},{"address":["sourcecred","git","HAS_PARENT","2","COMMIT","c430bd74455105f77215ece51945094ceeee6c86","2","COMMIT","6d5b3aa31ebb68a06ceb46bbd6cf49b6ccd6f5e6"],"dstIndex":2,"srcIndex":3},{"address":["sourcecred","git","HAS_PARENT","2","COMMIT","ecc889dc94cf6da17ae6eab5bb7b7155f577519d","2","COMMIT","ec91adb718a6045b492303f00d8e8beb957dc780"],"dstIndex":4,"srcIndex":5}],"nodes":[{"index":0},{"index":1},{"index":2},{"index":3},{"index":4},{"index":5}],"sortedNodeAddresses":[["sourcecred","git","COMMIT","0a223346b4e6dec0127b1e6aa892c4ee0424b66a"],["sourcecred","git","COMMIT","6bd1b4c0b719c22c688a74863be07a699b7b9b34"],["sourcecred","git","COMMIT","6d5b3aa31ebb68a06ceb46bbd6cf49b6ccd6f5e6"],["sourcecred","git","COMMIT","c430bd74455105f77215ece51945094ceeee6c86"],["sourcecred","git","COMMIT","ec91adb718a6045b492303f00d8e8beb957dc780"],["sourcecred","git","COMMIT","ecc889dc94cf6da17ae6eab5bb7b7155f577519d"]]}] \ No newline at end of file diff --git a/sharness/__snapshots__/example-github-load/data/sourcecred/example-github/graph.json b/sharness/__snapshots__/example-github-load/data/sourcecred/example-github/graph.json index b140991..007ca97 100644 --- a/sharness/__snapshots__/example-github-load/data/sourcecred/example-github/graph.json +++ b/sharness/__snapshots__/example-github-load/data/sourcecred/example-github/graph.json @@ -1 +1 @@ -[{"type":"sourcecred/graph","version":"0.4.0"},{"edges":[{"address":["sourcecred","git","HAS_PARENT","2","COMMIT","0a223346b4e6dec0127b1e6aa892c4ee0424b66a","2","COMMIT","ec91adb718a6045b492303f00d8e8beb957dc780"],"dstIndex":4,"srcIndex":0},{"address":["sourcecred","git","HAS_PARENT","2","COMMIT","0a223346b4e6dec0127b1e6aa892c4ee0424b66a","2","COMMIT","ecc889dc94cf6da17ae6eab5bb7b7155f577519d"],"dstIndex":5,"srcIndex":0},{"address":["sourcecred","git","HAS_PARENT","2","COMMIT","6bd1b4c0b719c22c688a74863be07a699b7b9b34","2","COMMIT","c430bd74455105f77215ece51945094ceeee6c86"],"dstIndex":3,"srcIndex":1},{"address":["sourcecred","git","HAS_PARENT","2","COMMIT","6d5b3aa31ebb68a06ceb46bbd6cf49b6ccd6f5e6","2","COMMIT","0a223346b4e6dec0127b1e6aa892c4ee0424b66a"],"dstIndex":0,"srcIndex":2},{"address":["sourcecred","git","HAS_PARENT","2","COMMIT","c430bd74455105f77215ece51945094ceeee6c86","2","COMMIT","6d5b3aa31ebb68a06ceb46bbd6cf49b6ccd6f5e6"],"dstIndex":2,"srcIndex":3},{"address":["sourcecred","git","HAS_PARENT","2","COMMIT","ecc889dc94cf6da17ae6eab5bb7b7155f577519d","2","COMMIT","ec91adb718a6045b492303f00d8e8beb957dc780"],"dstIndex":4,"srcIndex":5},{"address":["sourcecred","github","AUTHORS","5","sourcecred","github","USERLIKE","BOT","credbot","4","sourcecred","github","COMMIT","MDY6Q29tbWl0MTIzMjU1MDA2OmM0MzBiZDc0NDU1MTA1Zjc3MjE1ZWNlNTE5NDUwOTRjZWVlZTZjODY="],"dstIndex":28,"srcIndex":47},{"address":["sourcecred","github","AUTHORS","5","sourcecred","github","USERLIKE","BOT","credbot","8","sourcecred","github","COMMENT","ISSUE","sourcecred","example-github","6","417104047"],"dstIndex":21,"srcIndex":47},{"address":["sourcecred","github","AUTHORS","5","sourcecred","github","USERLIKE","USER","decentralion","4","sourcecred","github","COMMIT","MDY6Q29tbWl0MTIzMjU1MDA2OjBhMjIzMzQ2YjRlNmRlYzAxMjdiMWU2YWE4OTJjNGVlMDQyNGI2NmE="],"dstIndex":25,"srcIndex":48},{"address":["sourcecred","github","AUTHORS","5","sourcecred","github","USERLIKE","USER","decentralion","4","sourcecred","github","COMMIT","MDY6Q29tbWl0MTIzMjU1MDA2OjZkNWIzYWEzMWViYjY4YTA2Y2ViNDZiYmQ2Y2Y0OWI2Y2NkNmY1ZTY="],"dstIndex":27,"srcIndex":48},{"address":["sourcecred","github","AUTHORS","5","sourcecred","github","USERLIKE","USER","decentralion","4","sourcecred","github","COMMIT","MDY6Q29tbWl0MTIzMjU1MDA2OmVjOTFhZGI3MThhNjA0NWI0OTIzMDNmMDBkOGU4YmViOTU3ZGM3ODA="],"dstIndex":29,"srcIndex":48},{"address":["sourcecred","github","AUTHORS","5","sourcecred","github","USERLIKE","USER","decentralion","4","sourcecred","github","COMMIT","MDY6Q29tbWl0MTIzMjU1MDA2OmVjYzg4OWRjOTRjZjZkYTE3YWU2ZWFiNWJiN2I3MTU1ZjU3NzUxOWQ="],"dstIndex":30,"srcIndex":48},{"address":["sourcecred","github","AUTHORS","5","sourcecred","github","USERLIKE","USER","decentralion","6","sourcecred","github","ISSUE","sourcecred","example-github","1"],"dstIndex":31,"srcIndex":48},{"address":["sourcecred","github","AUTHORS","5","sourcecred","github","USERLIKE","USER","decentralion","6","sourcecred","github","ISSUE","sourcecred","example-github","10"],"dstIndex":32,"srcIndex":48},{"address":["sourcecred","github","AUTHORS","5","sourcecred","github","USERLIKE","USER","decentralion","6","sourcecred","github","ISSUE","sourcecred","example-github","12"],"dstIndex":34,"srcIndex":48},{"address":["sourcecred","github","AUTHORS","5","sourcecred","github","USERLIKE","USER","decentralion","6","sourcecred","github","ISSUE","sourcecred","example-github","13"],"dstIndex":35,"srcIndex":48},{"address":["sourcecred","github","AUTHORS","5","sourcecred","github","USERLIKE","USER","decentralion","6","sourcecred","github","ISSUE","sourcecred","example-github","2"],"dstIndex":36,"srcIndex":48},{"address":["sourcecred","github","AUTHORS","5","sourcecred","github","USERLIKE","USER","decentralion","6","sourcecred","github","ISSUE","sourcecred","example-github","4"],"dstIndex":37,"srcIndex":48},{"address":["sourcecred","github","AUTHORS","5","sourcecred","github","USERLIKE","USER","decentralion","6","sourcecred","github","ISSUE","sourcecred","example-github","6"],"dstIndex":38,"srcIndex":48},{"address":["sourcecred","github","AUTHORS","5","sourcecred","github","USERLIKE","USER","decentralion","6","sourcecred","github","ISSUE","sourcecred","example-github","7"],"dstIndex":39,"srcIndex":48},{"address":["sourcecred","github","AUTHORS","5","sourcecred","github","USERLIKE","USER","decentralion","6","sourcecred","github","ISSUE","sourcecred","example-github","8"],"dstIndex":40,"srcIndex":48},{"address":["sourcecred","github","AUTHORS","5","sourcecred","github","USERLIKE","USER","decentralion","6","sourcecred","github","PULL","sourcecred","example-github","3"],"dstIndex":41,"srcIndex":48},{"address":["sourcecred","github","AUTHORS","5","sourcecred","github","USERLIKE","USER","decentralion","6","sourcecred","github","PULL","sourcecred","example-github","5"],"dstIndex":42,"srcIndex":48},{"address":["sourcecred","github","AUTHORS","5","sourcecred","github","USERLIKE","USER","decentralion","6","sourcecred","github","PULL","sourcecred","example-github","9"],"dstIndex":43,"srcIndex":48},{"address":["sourcecred","github","AUTHORS","5","sourcecred","github","USERLIKE","USER","decentralion","8","sourcecred","github","COMMENT","ISSUE","sourcecred","example-github","11","420813621"],"dstIndex":9,"srcIndex":48},{"address":["sourcecred","github","AUTHORS","5","sourcecred","github","USERLIKE","USER","decentralion","8","sourcecred","github","COMMENT","ISSUE","sourcecred","example-github","2","373768703"],"dstIndex":10,"srcIndex":48},{"address":["sourcecred","github","AUTHORS","5","sourcecred","github","USERLIKE","USER","decentralion","8","sourcecred","github","COMMENT","ISSUE","sourcecred","example-github","2","373768850"],"dstIndex":11,"srcIndex":48},{"address":["sourcecred","github","AUTHORS","5","sourcecred","github","USERLIKE","USER","decentralion","8","sourcecred","github","COMMENT","ISSUE","sourcecred","example-github","2","385576185"],"dstIndex":12,"srcIndex":48},{"address":["sourcecred","github","AUTHORS","5","sourcecred","github","USERLIKE","USER","decentralion","8","sourcecred","github","COMMENT","ISSUE","sourcecred","example-github","2","385576220"],"dstIndex":13,"srcIndex":48},{"address":["sourcecred","github","AUTHORS","5","sourcecred","github","USERLIKE","USER","decentralion","8","sourcecred","github","COMMENT","ISSUE","sourcecred","example-github","2","385576248"],"dstIndex":14,"srcIndex":48},{"address":["sourcecred","github","AUTHORS","5","sourcecred","github","USERLIKE","USER","decentralion","8","sourcecred","github","COMMENT","ISSUE","sourcecred","example-github","2","385576273"],"dstIndex":15,"srcIndex":48},{"address":["sourcecred","github","AUTHORS","5","sourcecred","github","USERLIKE","USER","decentralion","8","sourcecred","github","COMMENT","ISSUE","sourcecred","example-github","2","385576920"],"dstIndex":16,"srcIndex":48},{"address":["sourcecred","github","AUTHORS","5","sourcecred","github","USERLIKE","USER","decentralion","8","sourcecred","github","COMMENT","ISSUE","sourcecred","example-github","2","385576936"],"dstIndex":17,"srcIndex":48},{"address":["sourcecred","github","AUTHORS","5","sourcecred","github","USERLIKE","USER","decentralion","8","sourcecred","github","COMMENT","ISSUE","sourcecred","example-github","6","373768442"],"dstIndex":18,"srcIndex":48},{"address":["sourcecred","github","AUTHORS","5","sourcecred","github","USERLIKE","USER","decentralion","8","sourcecred","github","COMMENT","ISSUE","sourcecred","example-github","6","373768538"],"dstIndex":19,"srcIndex":48},{"address":["sourcecred","github","AUTHORS","5","sourcecred","github","USERLIKE","USER","decentralion","8","sourcecred","github","COMMENT","ISSUE","sourcecred","example-github","6","385223316"],"dstIndex":20,"srcIndex":48},{"address":["sourcecred","github","AUTHORS","5","sourcecred","github","USERLIKE","USER","decentralion","8","sourcecred","github","COMMENT","PULL","sourcecred","example-github","3","369162222"],"dstIndex":22,"srcIndex":48},{"address":["sourcecred","github","AUTHORS","5","sourcecred","github","USERLIKE","USER","wchargin","6","sourcecred","github","ISSUE","sourcecred","example-github","10"],"dstIndex":32,"srcIndex":49},{"address":["sourcecred","github","AUTHORS","5","sourcecred","github","USERLIKE","USER","wchargin","6","sourcecred","github","ISSUE","sourcecred","example-github","11"],"dstIndex":33,"srcIndex":49},{"address":["sourcecred","github","AUTHORS","5","sourcecred","github","USERLIKE","USER","wchargin","6","sourcecred","github","PULL","sourcecred","example-github","9"],"dstIndex":43,"srcIndex":49},{"address":["sourcecred","github","AUTHORS","5","sourcecred","github","USERLIKE","USER","wchargin","7","sourcecred","github","REVIEW","sourcecred","example-github","5","100313899"],"dstIndex":45,"srcIndex":49},{"address":["sourcecred","github","AUTHORS","5","sourcecred","github","USERLIKE","USER","wchargin","7","sourcecred","github","REVIEW","sourcecred","example-github","5","100314038"],"dstIndex":46,"srcIndex":49},{"address":["sourcecred","github","AUTHORS","5","sourcecred","github","USERLIKE","USER","wchargin","8","sourcecred","github","COMMENT","ISSUE","sourcecred","example-github","11","420813013"],"dstIndex":7,"srcIndex":49},{"address":["sourcecred","github","AUTHORS","5","sourcecred","github","USERLIKE","USER","wchargin","8","sourcecred","github","COMMENT","PULL","sourcecred","example-github","5","396430464"],"dstIndex":23,"srcIndex":49},{"address":["sourcecred","github","AUTHORS","5","sourcecred","github","USERLIKE","USER","wchargin","9","sourcecred","github","COMMENT","REVIEW","sourcecred","example-github","5","100313899","171460198"],"dstIndex":24,"srcIndex":49},{"address":["sourcecred","github","CORRESPONDS_TO_COMMIT_TYPE","4","sourcecred","github","COMMIT","MDY6Q29tbWl0MTIzMjU1MDA2OjBhMjIzMzQ2YjRlNmRlYzAxMjdiMWU2YWE4OTJjNGVlMDQyNGI2NmE="],"dstIndex":0,"srcIndex":25},{"address":["sourcecred","github","CORRESPONDS_TO_COMMIT_TYPE","4","sourcecred","github","COMMIT","MDY6Q29tbWl0MTIzMjU1MDA2OjZiZDFiNGMwYjcxOWMyMmM2ODhhNzQ4NjNiZTA3YTY5OWI3YjliMzQ="],"dstIndex":1,"srcIndex":26},{"address":["sourcecred","github","CORRESPONDS_TO_COMMIT_TYPE","4","sourcecred","github","COMMIT","MDY6Q29tbWl0MTIzMjU1MDA2OjZkNWIzYWEzMWViYjY4YTA2Y2ViNDZiYmQ2Y2Y0OWI2Y2NkNmY1ZTY="],"dstIndex":2,"srcIndex":27},{"address":["sourcecred","github","CORRESPONDS_TO_COMMIT_TYPE","4","sourcecred","github","COMMIT","MDY6Q29tbWl0MTIzMjU1MDA2OmM0MzBiZDc0NDU1MTA1Zjc3MjE1ZWNlNTE5NDUwOTRjZWVlZTZjODY="],"dstIndex":3,"srcIndex":28},{"address":["sourcecred","github","CORRESPONDS_TO_COMMIT_TYPE","4","sourcecred","github","COMMIT","MDY6Q29tbWl0MTIzMjU1MDA2OmVjOTFhZGI3MThhNjA0NWI0OTIzMDNmMDBkOGU4YmViOTU3ZGM3ODA="],"dstIndex":4,"srcIndex":29},{"address":["sourcecred","github","CORRESPONDS_TO_COMMIT_TYPE","4","sourcecred","github","COMMIT","MDY6Q29tbWl0MTIzMjU1MDA2OmVjYzg4OWRjOTRjZjZkYTE3YWU2ZWFiNWJiN2I3MTU1ZjU3NzUxOWQ="],"dstIndex":5,"srcIndex":30},{"address":["sourcecred","github","HAS_PARENT","6","sourcecred","github","ISSUE","sourcecred","example-github","1"],"dstIndex":44,"srcIndex":31},{"address":["sourcecred","github","HAS_PARENT","6","sourcecred","github","ISSUE","sourcecred","example-github","10"],"dstIndex":44,"srcIndex":32},{"address":["sourcecred","github","HAS_PARENT","6","sourcecred","github","ISSUE","sourcecred","example-github","11"],"dstIndex":44,"srcIndex":33},{"address":["sourcecred","github","HAS_PARENT","6","sourcecred","github","ISSUE","sourcecred","example-github","12"],"dstIndex":44,"srcIndex":34},{"address":["sourcecred","github","HAS_PARENT","6","sourcecred","github","ISSUE","sourcecred","example-github","13"],"dstIndex":44,"srcIndex":35},{"address":["sourcecred","github","HAS_PARENT","6","sourcecred","github","ISSUE","sourcecred","example-github","2"],"dstIndex":44,"srcIndex":36},{"address":["sourcecred","github","HAS_PARENT","6","sourcecred","github","ISSUE","sourcecred","example-github","4"],"dstIndex":44,"srcIndex":37},{"address":["sourcecred","github","HAS_PARENT","6","sourcecred","github","ISSUE","sourcecred","example-github","6"],"dstIndex":44,"srcIndex":38},{"address":["sourcecred","github","HAS_PARENT","6","sourcecred","github","ISSUE","sourcecred","example-github","7"],"dstIndex":44,"srcIndex":39},{"address":["sourcecred","github","HAS_PARENT","6","sourcecred","github","ISSUE","sourcecred","example-github","8"],"dstIndex":44,"srcIndex":40},{"address":["sourcecred","github","HAS_PARENT","6","sourcecred","github","PULL","sourcecred","example-github","3"],"dstIndex":44,"srcIndex":41},{"address":["sourcecred","github","HAS_PARENT","6","sourcecred","github","PULL","sourcecred","example-github","5"],"dstIndex":44,"srcIndex":42},{"address":["sourcecred","github","HAS_PARENT","6","sourcecred","github","PULL","sourcecred","example-github","9"],"dstIndex":44,"srcIndex":43},{"address":["sourcecred","github","HAS_PARENT","7","sourcecred","github","REVIEW","sourcecred","example-github","5","100313899"],"dstIndex":42,"srcIndex":45},{"address":["sourcecred","github","HAS_PARENT","7","sourcecred","github","REVIEW","sourcecred","example-github","5","100314038"],"dstIndex":42,"srcIndex":46},{"address":["sourcecred","github","HAS_PARENT","8","sourcecred","github","COMMENT","ISSUE","sourcecred","example-github","11","420811872"],"dstIndex":33,"srcIndex":6},{"address":["sourcecred","github","HAS_PARENT","8","sourcecred","github","COMMENT","ISSUE","sourcecred","example-github","11","420813013"],"dstIndex":33,"srcIndex":7},{"address":["sourcecred","github","HAS_PARENT","8","sourcecred","github","COMMENT","ISSUE","sourcecred","example-github","11","420813206"],"dstIndex":33,"srcIndex":8},{"address":["sourcecred","github","HAS_PARENT","8","sourcecred","github","COMMENT","ISSUE","sourcecred","example-github","11","420813621"],"dstIndex":33,"srcIndex":9},{"address":["sourcecred","github","HAS_PARENT","8","sourcecred","github","COMMENT","ISSUE","sourcecred","example-github","2","373768703"],"dstIndex":36,"srcIndex":10},{"address":["sourcecred","github","HAS_PARENT","8","sourcecred","github","COMMENT","ISSUE","sourcecred","example-github","2","373768850"],"dstIndex":36,"srcIndex":11},{"address":["sourcecred","github","HAS_PARENT","8","sourcecred","github","COMMENT","ISSUE","sourcecred","example-github","2","385576185"],"dstIndex":36,"srcIndex":12},{"address":["sourcecred","github","HAS_PARENT","8","sourcecred","github","COMMENT","ISSUE","sourcecred","example-github","2","385576220"],"dstIndex":36,"srcIndex":13},{"address":["sourcecred","github","HAS_PARENT","8","sourcecred","github","COMMENT","ISSUE","sourcecred","example-github","2","385576248"],"dstIndex":36,"srcIndex":14},{"address":["sourcecred","github","HAS_PARENT","8","sourcecred","github","COMMENT","ISSUE","sourcecred","example-github","2","385576273"],"dstIndex":36,"srcIndex":15},{"address":["sourcecred","github","HAS_PARENT","8","sourcecred","github","COMMENT","ISSUE","sourcecred","example-github","2","385576920"],"dstIndex":36,"srcIndex":16},{"address":["sourcecred","github","HAS_PARENT","8","sourcecred","github","COMMENT","ISSUE","sourcecred","example-github","2","385576936"],"dstIndex":36,"srcIndex":17},{"address":["sourcecred","github","HAS_PARENT","8","sourcecred","github","COMMENT","ISSUE","sourcecred","example-github","6","373768442"],"dstIndex":38,"srcIndex":18},{"address":["sourcecred","github","HAS_PARENT","8","sourcecred","github","COMMENT","ISSUE","sourcecred","example-github","6","373768538"],"dstIndex":38,"srcIndex":19},{"address":["sourcecred","github","HAS_PARENT","8","sourcecred","github","COMMENT","ISSUE","sourcecred","example-github","6","385223316"],"dstIndex":38,"srcIndex":20},{"address":["sourcecred","github","HAS_PARENT","8","sourcecred","github","COMMENT","ISSUE","sourcecred","example-github","6","417104047"],"dstIndex":38,"srcIndex":21},{"address":["sourcecred","github","HAS_PARENT","8","sourcecred","github","COMMENT","PULL","sourcecred","example-github","3","369162222"],"dstIndex":41,"srcIndex":22},{"address":["sourcecred","github","HAS_PARENT","8","sourcecred","github","COMMENT","PULL","sourcecred","example-github","5","396430464"],"dstIndex":42,"srcIndex":23},{"address":["sourcecred","github","HAS_PARENT","9","sourcecred","github","COMMENT","REVIEW","sourcecred","example-github","5","100313899","171460198"],"dstIndex":45,"srcIndex":24},{"address":["sourcecred","github","MERGED_AS","6","sourcecred","github","PULL","sourcecred","example-github","3"],"dstIndex":25,"srcIndex":41},{"address":["sourcecred","github","MERGED_AS","6","sourcecred","github","PULL","sourcecred","example-github","5"],"dstIndex":27,"srcIndex":42},{"address":["sourcecred","github","REACTS","HEART","5","sourcecred","github","USERLIKE","USER","decentralion","6","sourcecred","github","ISSUE","sourcecred","example-github","1"],"dstIndex":31,"srcIndex":48},{"address":["sourcecred","github","REACTS","HEART","5","sourcecred","github","USERLIKE","USER","decentralion","6","sourcecred","github","ISSUE","sourcecred","example-github","13"],"dstIndex":35,"srcIndex":48},{"address":["sourcecred","github","REACTS","HEART","5","sourcecred","github","USERLIKE","USER","decentralion","6","sourcecred","github","PULL","sourcecred","example-github","9"],"dstIndex":43,"srcIndex":48},{"address":["sourcecred","github","REACTS","HEART","5","sourcecred","github","USERLIKE","USER","decentralion","8","sourcecred","github","COMMENT","PULL","sourcecred","example-github","5","396430464"],"dstIndex":23,"srcIndex":48},{"address":["sourcecred","github","REACTS","HEART","5","sourcecred","github","USERLIKE","USER","decentralion","9","sourcecred","github","COMMENT","REVIEW","sourcecred","example-github","5","100313899","171460198"],"dstIndex":24,"srcIndex":48},{"address":["sourcecred","github","REACTS","HEART","5","sourcecred","github","USERLIKE","USER","wchargin","9","sourcecred","github","COMMENT","REVIEW","sourcecred","example-github","5","100313899","171460198"],"dstIndex":24,"srcIndex":49},{"address":["sourcecred","github","REACTS","HOORAY","5","sourcecred","github","USERLIKE","USER","decentralion","6","sourcecred","github","ISSUE","sourcecred","example-github","13"],"dstIndex":35,"srcIndex":48},{"address":["sourcecred","github","REACTS","HOORAY","5","sourcecred","github","USERLIKE","USER","decentralion","8","sourcecred","github","COMMENT","ISSUE","sourcecred","example-github","11","420813206"],"dstIndex":8,"srcIndex":48},{"address":["sourcecred","github","REACTS","HOORAY","5","sourcecred","github","USERLIKE","USER","decentralion","9","sourcecred","github","COMMENT","REVIEW","sourcecred","example-github","5","100313899","171460198"],"dstIndex":24,"srcIndex":48},{"address":["sourcecred","github","REACTS","HOORAY","5","sourcecred","github","USERLIKE","USER","wchargin","9","sourcecred","github","COMMENT","REVIEW","sourcecred","example-github","5","100313899","171460198"],"dstIndex":24,"srcIndex":49},{"address":["sourcecred","github","REACTS","ROCKET","5","sourcecred","github","USERLIKE","USER","decentralion","6","sourcecred","github","ISSUE","sourcecred","example-github","13"],"dstIndex":35,"srcIndex":48},{"address":["sourcecred","github","REACTS","THUMBS_UP","5","sourcecred","github","USERLIKE","USER","decentralion","6","sourcecred","github","ISSUE","sourcecred","example-github","12"],"dstIndex":34,"srcIndex":48},{"address":["sourcecred","github","REACTS","THUMBS_UP","5","sourcecred","github","USERLIKE","USER","decentralion","6","sourcecred","github","ISSUE","sourcecred","example-github","13"],"dstIndex":35,"srcIndex":48},{"address":["sourcecred","github","REACTS","THUMBS_UP","5","sourcecred","github","USERLIKE","USER","decentralion","6","sourcecred","github","PULL","sourcecred","example-github","9"],"dstIndex":43,"srcIndex":48},{"address":["sourcecred","github","REACTS","THUMBS_UP","5","sourcecred","github","USERLIKE","USER","decentralion","8","sourcecred","github","COMMENT","ISSUE","sourcecred","example-github","11","420813206"],"dstIndex":8,"srcIndex":48},{"address":["sourcecred","github","REACTS","THUMBS_UP","5","sourcecred","github","USERLIKE","USER","decentralion","9","sourcecred","github","COMMENT","REVIEW","sourcecred","example-github","5","100313899","171460198"],"dstIndex":24,"srcIndex":48},{"address":["sourcecred","github","REACTS","THUMBS_UP","5","sourcecred","github","USERLIKE","USER","wchargin","9","sourcecred","github","COMMENT","REVIEW","sourcecred","example-github","5","100313899","171460198"],"dstIndex":24,"srcIndex":49},{"address":["sourcecred","github","REFERENCES","4","sourcecred","github","COMMIT","MDY6Q29tbWl0MTIzMjU1MDA2OjBhMjIzMzQ2YjRlNmRlYzAxMjdiMWU2YWE4OTJjNGVlMDQyNGI2NmE=","6","sourcecred","github","PULL","sourcecred","example-github","3"],"dstIndex":41,"srcIndex":25},{"address":["sourcecred","github","REFERENCES","4","sourcecred","github","COMMIT","MDY6Q29tbWl0MTIzMjU1MDA2OjZiZDFiNGMwYjcxOWMyMmM2ODhhNzQ4NjNiZTA3YTY5OWI3YjliMzQ=","5","sourcecred","github","USERLIKE","USER","wchargin"],"dstIndex":49,"srcIndex":26},{"address":["sourcecred","github","REFERENCES","4","sourcecred","github","COMMIT","MDY6Q29tbWl0MTIzMjU1MDA2OjZkNWIzYWEzMWViYjY4YTA2Y2ViNDZiYmQ2Y2Y0OWI2Y2NkNmY1ZTY=","6","sourcecred","github","PULL","sourcecred","example-github","5"],"dstIndex":42,"srcIndex":27},{"address":["sourcecred","github","REFERENCES","4","sourcecred","github","COMMIT","MDY6Q29tbWl0MTIzMjU1MDA2OmM0MzBiZDc0NDU1MTA1Zjc3MjE1ZWNlNTE5NDUwOTRjZWVlZTZjODY=","5","sourcecred","github","USERLIKE","USER","wchargin"],"dstIndex":49,"srcIndex":28},{"address":["sourcecred","github","REFERENCES","6","sourcecred","github","ISSUE","sourcecred","example-github","10","6","sourcecred","github","ISSUE","sourcecred","example-github","10"],"dstIndex":32,"srcIndex":32},{"address":["sourcecred","github","REFERENCES","6","sourcecred","github","ISSUE","sourcecred","example-github","10","6","sourcecred","github","ISSUE","sourcecred","example-github","2"],"dstIndex":36,"srcIndex":32},{"address":["sourcecred","github","REFERENCES","6","sourcecred","github","ISSUE","sourcecred","example-github","12","4","sourcecred","github","COMMIT","MDY6Q29tbWl0MTIzMjU1MDA2OmVjOTFhZGI3MThhNjA0NWI0OTIzMDNmMDBkOGU4YmViOTU3ZGM3ODA="],"dstIndex":29,"srcIndex":34},{"address":["sourcecred","github","REFERENCES","6","sourcecred","github","ISSUE","sourcecred","example-github","12","4","sourcecred","github","COMMIT","MDY6Q29tbWl0MTIzMjU1MDA2OmVjYzg4OWRjOTRjZjZkYTE3YWU2ZWFiNWJiN2I3MTU1ZjU3NzUxOWQ="],"dstIndex":30,"srcIndex":34},{"address":["sourcecred","github","REFERENCES","6","sourcecred","github","ISSUE","sourcecred","example-github","2","6","sourcecred","github","ISSUE","sourcecred","example-github","1"],"dstIndex":31,"srcIndex":36},{"address":["sourcecred","github","REFERENCES","6","sourcecred","github","PULL","sourcecred","example-github","5","5","sourcecred","github","USERLIKE","USER","wchargin"],"dstIndex":49,"srcIndex":42},{"address":["sourcecred","github","REFERENCES","8","sourcecred","github","COMMENT","ISSUE","sourcecred","example-github","2","373768703","6","sourcecred","github","ISSUE","sourcecred","example-github","6"],"dstIndex":38,"srcIndex":10},{"address":["sourcecred","github","REFERENCES","8","sourcecred","github","COMMENT","ISSUE","sourcecred","example-github","2","373768850","8","sourcecred","github","COMMENT","ISSUE","sourcecred","example-github","6","373768538"],"dstIndex":19,"srcIndex":11},{"address":["sourcecred","github","REFERENCES","8","sourcecred","github","COMMENT","ISSUE","sourcecred","example-github","2","385576185","6","sourcecred","github","PULL","sourcecred","example-github","5"],"dstIndex":42,"srcIndex":12},{"address":["sourcecred","github","REFERENCES","8","sourcecred","github","COMMENT","ISSUE","sourcecred","example-github","2","385576220","7","sourcecred","github","REVIEW","sourcecred","example-github","5","100313899"],"dstIndex":45,"srcIndex":13},{"address":["sourcecred","github","REFERENCES","8","sourcecred","github","COMMENT","ISSUE","sourcecred","example-github","2","385576248","9","sourcecred","github","COMMENT","REVIEW","sourcecred","example-github","5","100313899","171460198"],"dstIndex":24,"srcIndex":14},{"address":["sourcecred","github","REFERENCES","8","sourcecred","github","COMMENT","ISSUE","sourcecred","example-github","2","385576273","5","sourcecred","github","USERLIKE","USER","wchargin"],"dstIndex":49,"srcIndex":15},{"address":["sourcecred","github","REFERENCES","8","sourcecred","github","COMMENT","ISSUE","sourcecred","example-github","2","385576920","6","sourcecred","github","ISSUE","sourcecred","example-github","1"],"dstIndex":31,"srcIndex":16},{"address":["sourcecred","github","REFERENCES","8","sourcecred","github","COMMENT","ISSUE","sourcecred","example-github","2","385576920","6","sourcecred","github","ISSUE","sourcecred","example-github","2"],"dstIndex":36,"srcIndex":16},{"address":["sourcecred","github","REFERENCES","8","sourcecred","github","COMMENT","ISSUE","sourcecred","example-github","2","385576920","6","sourcecred","github","PULL","sourcecred","example-github","3"],"dstIndex":41,"srcIndex":16},{"address":["sourcecred","github","REFERENCES","8","sourcecred","github","COMMENT","ISSUE","sourcecred","example-github","2","385576920","7","sourcecred","github","REVIEW","sourcecred","example-github","5","100313899"],"dstIndex":45,"srcIndex":16},{"address":["sourcecred","github","REFERENCES","8","sourcecred","github","COMMENT","ISSUE","sourcecred","example-github","2","385576920","9","sourcecred","github","COMMENT","REVIEW","sourcecred","example-github","5","100313899","171460198"],"dstIndex":24,"srcIndex":16},{"address":["sourcecred","github","REFERENCES","8","sourcecred","github","COMMENT","ISSUE","sourcecred","example-github","6","385223316","6","sourcecred","github","ISSUE","sourcecred","example-github","2"],"dstIndex":36,"srcIndex":20},{"address":["sourcecred","github","REFERENCES","8","sourcecred","github","COMMENT","PULL","sourcecred","example-github","3","369162222","6","sourcecred","github","ISSUE","sourcecred","example-github","2"],"dstIndex":36,"srcIndex":22}],"nodes":[["sourcecred","git","COMMIT","0a223346b4e6dec0127b1e6aa892c4ee0424b66a"],["sourcecred","git","COMMIT","6bd1b4c0b719c22c688a74863be07a699b7b9b34"],["sourcecred","git","COMMIT","6d5b3aa31ebb68a06ceb46bbd6cf49b6ccd6f5e6"],["sourcecred","git","COMMIT","c430bd74455105f77215ece51945094ceeee6c86"],["sourcecred","git","COMMIT","ec91adb718a6045b492303f00d8e8beb957dc780"],["sourcecred","git","COMMIT","ecc889dc94cf6da17ae6eab5bb7b7155f577519d"],["sourcecred","github","COMMENT","ISSUE","sourcecred","example-github","11","420811872"],["sourcecred","github","COMMENT","ISSUE","sourcecred","example-github","11","420813013"],["sourcecred","github","COMMENT","ISSUE","sourcecred","example-github","11","420813206"],["sourcecred","github","COMMENT","ISSUE","sourcecred","example-github","11","420813621"],["sourcecred","github","COMMENT","ISSUE","sourcecred","example-github","2","373768703"],["sourcecred","github","COMMENT","ISSUE","sourcecred","example-github","2","373768850"],["sourcecred","github","COMMENT","ISSUE","sourcecred","example-github","2","385576185"],["sourcecred","github","COMMENT","ISSUE","sourcecred","example-github","2","385576220"],["sourcecred","github","COMMENT","ISSUE","sourcecred","example-github","2","385576248"],["sourcecred","github","COMMENT","ISSUE","sourcecred","example-github","2","385576273"],["sourcecred","github","COMMENT","ISSUE","sourcecred","example-github","2","385576920"],["sourcecred","github","COMMENT","ISSUE","sourcecred","example-github","2","385576936"],["sourcecred","github","COMMENT","ISSUE","sourcecred","example-github","6","373768442"],["sourcecred","github","COMMENT","ISSUE","sourcecred","example-github","6","373768538"],["sourcecred","github","COMMENT","ISSUE","sourcecred","example-github","6","385223316"],["sourcecred","github","COMMENT","ISSUE","sourcecred","example-github","6","417104047"],["sourcecred","github","COMMENT","PULL","sourcecred","example-github","3","369162222"],["sourcecred","github","COMMENT","PULL","sourcecred","example-github","5","396430464"],["sourcecred","github","COMMENT","REVIEW","sourcecred","example-github","5","100313899","171460198"],["sourcecred","github","COMMIT","MDY6Q29tbWl0MTIzMjU1MDA2OjBhMjIzMzQ2YjRlNmRlYzAxMjdiMWU2YWE4OTJjNGVlMDQyNGI2NmE="],["sourcecred","github","COMMIT","MDY6Q29tbWl0MTIzMjU1MDA2OjZiZDFiNGMwYjcxOWMyMmM2ODhhNzQ4NjNiZTA3YTY5OWI3YjliMzQ="],["sourcecred","github","COMMIT","MDY6Q29tbWl0MTIzMjU1MDA2OjZkNWIzYWEzMWViYjY4YTA2Y2ViNDZiYmQ2Y2Y0OWI2Y2NkNmY1ZTY="],["sourcecred","github","COMMIT","MDY6Q29tbWl0MTIzMjU1MDA2OmM0MzBiZDc0NDU1MTA1Zjc3MjE1ZWNlNTE5NDUwOTRjZWVlZTZjODY="],["sourcecred","github","COMMIT","MDY6Q29tbWl0MTIzMjU1MDA2OmVjOTFhZGI3MThhNjA0NWI0OTIzMDNmMDBkOGU4YmViOTU3ZGM3ODA="],["sourcecred","github","COMMIT","MDY6Q29tbWl0MTIzMjU1MDA2OmVjYzg4OWRjOTRjZjZkYTE3YWU2ZWFiNWJiN2I3MTU1ZjU3NzUxOWQ="],["sourcecred","github","ISSUE","sourcecred","example-github","1"],["sourcecred","github","ISSUE","sourcecred","example-github","10"],["sourcecred","github","ISSUE","sourcecred","example-github","11"],["sourcecred","github","ISSUE","sourcecred","example-github","12"],["sourcecred","github","ISSUE","sourcecred","example-github","13"],["sourcecred","github","ISSUE","sourcecred","example-github","2"],["sourcecred","github","ISSUE","sourcecred","example-github","4"],["sourcecred","github","ISSUE","sourcecred","example-github","6"],["sourcecred","github","ISSUE","sourcecred","example-github","7"],["sourcecred","github","ISSUE","sourcecred","example-github","8"],["sourcecred","github","PULL","sourcecred","example-github","3"],["sourcecred","github","PULL","sourcecred","example-github","5"],["sourcecred","github","PULL","sourcecred","example-github","9"],["sourcecred","github","REPO","sourcecred","example-github"],["sourcecred","github","REVIEW","sourcecred","example-github","5","100313899"],["sourcecred","github","REVIEW","sourcecred","example-github","5","100314038"],["sourcecred","github","USERLIKE","BOT","credbot"],["sourcecred","github","USERLIKE","USER","decentralion"],["sourcecred","github","USERLIKE","USER","wchargin"]]}] \ No newline at end of file +[{"type":"sourcecred/graph","version":"0.5.0"},{"edges":[{"address":["sourcecred","git","HAS_PARENT","2","COMMIT","0a223346b4e6dec0127b1e6aa892c4ee0424b66a","2","COMMIT","ec91adb718a6045b492303f00d8e8beb957dc780"],"dstIndex":4,"srcIndex":0},{"address":["sourcecred","git","HAS_PARENT","2","COMMIT","0a223346b4e6dec0127b1e6aa892c4ee0424b66a","2","COMMIT","ecc889dc94cf6da17ae6eab5bb7b7155f577519d"],"dstIndex":5,"srcIndex":0},{"address":["sourcecred","git","HAS_PARENT","2","COMMIT","6bd1b4c0b719c22c688a74863be07a699b7b9b34","2","COMMIT","c430bd74455105f77215ece51945094ceeee6c86"],"dstIndex":3,"srcIndex":1},{"address":["sourcecred","git","HAS_PARENT","2","COMMIT","6d5b3aa31ebb68a06ceb46bbd6cf49b6ccd6f5e6","2","COMMIT","0a223346b4e6dec0127b1e6aa892c4ee0424b66a"],"dstIndex":0,"srcIndex":2},{"address":["sourcecred","git","HAS_PARENT","2","COMMIT","c430bd74455105f77215ece51945094ceeee6c86","2","COMMIT","6d5b3aa31ebb68a06ceb46bbd6cf49b6ccd6f5e6"],"dstIndex":2,"srcIndex":3},{"address":["sourcecred","git","HAS_PARENT","2","COMMIT","ecc889dc94cf6da17ae6eab5bb7b7155f577519d","2","COMMIT","ec91adb718a6045b492303f00d8e8beb957dc780"],"dstIndex":4,"srcIndex":5},{"address":["sourcecred","github","AUTHORS","5","sourcecred","github","USERLIKE","BOT","credbot","4","sourcecred","github","COMMIT","MDY6Q29tbWl0MTIzMjU1MDA2OmM0MzBiZDc0NDU1MTA1Zjc3MjE1ZWNlNTE5NDUwOTRjZWVlZTZjODY="],"dstIndex":28,"srcIndex":47},{"address":["sourcecred","github","AUTHORS","5","sourcecred","github","USERLIKE","BOT","credbot","8","sourcecred","github","COMMENT","ISSUE","sourcecred","example-github","6","417104047"],"dstIndex":21,"srcIndex":47},{"address":["sourcecred","github","AUTHORS","5","sourcecred","github","USERLIKE","USER","decentralion","4","sourcecred","github","COMMIT","MDY6Q29tbWl0MTIzMjU1MDA2OjBhMjIzMzQ2YjRlNmRlYzAxMjdiMWU2YWE4OTJjNGVlMDQyNGI2NmE="],"dstIndex":25,"srcIndex":48},{"address":["sourcecred","github","AUTHORS","5","sourcecred","github","USERLIKE","USER","decentralion","4","sourcecred","github","COMMIT","MDY6Q29tbWl0MTIzMjU1MDA2OjZkNWIzYWEzMWViYjY4YTA2Y2ViNDZiYmQ2Y2Y0OWI2Y2NkNmY1ZTY="],"dstIndex":27,"srcIndex":48},{"address":["sourcecred","github","AUTHORS","5","sourcecred","github","USERLIKE","USER","decentralion","4","sourcecred","github","COMMIT","MDY6Q29tbWl0MTIzMjU1MDA2OmVjOTFhZGI3MThhNjA0NWI0OTIzMDNmMDBkOGU4YmViOTU3ZGM3ODA="],"dstIndex":29,"srcIndex":48},{"address":["sourcecred","github","AUTHORS","5","sourcecred","github","USERLIKE","USER","decentralion","4","sourcecred","github","COMMIT","MDY6Q29tbWl0MTIzMjU1MDA2OmVjYzg4OWRjOTRjZjZkYTE3YWU2ZWFiNWJiN2I3MTU1ZjU3NzUxOWQ="],"dstIndex":30,"srcIndex":48},{"address":["sourcecred","github","AUTHORS","5","sourcecred","github","USERLIKE","USER","decentralion","6","sourcecred","github","ISSUE","sourcecred","example-github","1"],"dstIndex":31,"srcIndex":48},{"address":["sourcecred","github","AUTHORS","5","sourcecred","github","USERLIKE","USER","decentralion","6","sourcecred","github","ISSUE","sourcecred","example-github","10"],"dstIndex":32,"srcIndex":48},{"address":["sourcecred","github","AUTHORS","5","sourcecred","github","USERLIKE","USER","decentralion","6","sourcecred","github","ISSUE","sourcecred","example-github","12"],"dstIndex":34,"srcIndex":48},{"address":["sourcecred","github","AUTHORS","5","sourcecred","github","USERLIKE","USER","decentralion","6","sourcecred","github","ISSUE","sourcecred","example-github","13"],"dstIndex":35,"srcIndex":48},{"address":["sourcecred","github","AUTHORS","5","sourcecred","github","USERLIKE","USER","decentralion","6","sourcecred","github","ISSUE","sourcecred","example-github","2"],"dstIndex":36,"srcIndex":48},{"address":["sourcecred","github","AUTHORS","5","sourcecred","github","USERLIKE","USER","decentralion","6","sourcecred","github","ISSUE","sourcecred","example-github","4"],"dstIndex":37,"srcIndex":48},{"address":["sourcecred","github","AUTHORS","5","sourcecred","github","USERLIKE","USER","decentralion","6","sourcecred","github","ISSUE","sourcecred","example-github","6"],"dstIndex":38,"srcIndex":48},{"address":["sourcecred","github","AUTHORS","5","sourcecred","github","USERLIKE","USER","decentralion","6","sourcecred","github","ISSUE","sourcecred","example-github","7"],"dstIndex":39,"srcIndex":48},{"address":["sourcecred","github","AUTHORS","5","sourcecred","github","USERLIKE","USER","decentralion","6","sourcecred","github","ISSUE","sourcecred","example-github","8"],"dstIndex":40,"srcIndex":48},{"address":["sourcecred","github","AUTHORS","5","sourcecred","github","USERLIKE","USER","decentralion","6","sourcecred","github","PULL","sourcecred","example-github","3"],"dstIndex":41,"srcIndex":48},{"address":["sourcecred","github","AUTHORS","5","sourcecred","github","USERLIKE","USER","decentralion","6","sourcecred","github","PULL","sourcecred","example-github","5"],"dstIndex":42,"srcIndex":48},{"address":["sourcecred","github","AUTHORS","5","sourcecred","github","USERLIKE","USER","decentralion","6","sourcecred","github","PULL","sourcecred","example-github","9"],"dstIndex":43,"srcIndex":48},{"address":["sourcecred","github","AUTHORS","5","sourcecred","github","USERLIKE","USER","decentralion","8","sourcecred","github","COMMENT","ISSUE","sourcecred","example-github","11","420813621"],"dstIndex":9,"srcIndex":48},{"address":["sourcecred","github","AUTHORS","5","sourcecred","github","USERLIKE","USER","decentralion","8","sourcecred","github","COMMENT","ISSUE","sourcecred","example-github","2","373768703"],"dstIndex":10,"srcIndex":48},{"address":["sourcecred","github","AUTHORS","5","sourcecred","github","USERLIKE","USER","decentralion","8","sourcecred","github","COMMENT","ISSUE","sourcecred","example-github","2","373768850"],"dstIndex":11,"srcIndex":48},{"address":["sourcecred","github","AUTHORS","5","sourcecred","github","USERLIKE","USER","decentralion","8","sourcecred","github","COMMENT","ISSUE","sourcecred","example-github","2","385576185"],"dstIndex":12,"srcIndex":48},{"address":["sourcecred","github","AUTHORS","5","sourcecred","github","USERLIKE","USER","decentralion","8","sourcecred","github","COMMENT","ISSUE","sourcecred","example-github","2","385576220"],"dstIndex":13,"srcIndex":48},{"address":["sourcecred","github","AUTHORS","5","sourcecred","github","USERLIKE","USER","decentralion","8","sourcecred","github","COMMENT","ISSUE","sourcecred","example-github","2","385576248"],"dstIndex":14,"srcIndex":48},{"address":["sourcecred","github","AUTHORS","5","sourcecred","github","USERLIKE","USER","decentralion","8","sourcecred","github","COMMENT","ISSUE","sourcecred","example-github","2","385576273"],"dstIndex":15,"srcIndex":48},{"address":["sourcecred","github","AUTHORS","5","sourcecred","github","USERLIKE","USER","decentralion","8","sourcecred","github","COMMENT","ISSUE","sourcecred","example-github","2","385576920"],"dstIndex":16,"srcIndex":48},{"address":["sourcecred","github","AUTHORS","5","sourcecred","github","USERLIKE","USER","decentralion","8","sourcecred","github","COMMENT","ISSUE","sourcecred","example-github","2","385576936"],"dstIndex":17,"srcIndex":48},{"address":["sourcecred","github","AUTHORS","5","sourcecred","github","USERLIKE","USER","decentralion","8","sourcecred","github","COMMENT","ISSUE","sourcecred","example-github","6","373768442"],"dstIndex":18,"srcIndex":48},{"address":["sourcecred","github","AUTHORS","5","sourcecred","github","USERLIKE","USER","decentralion","8","sourcecred","github","COMMENT","ISSUE","sourcecred","example-github","6","373768538"],"dstIndex":19,"srcIndex":48},{"address":["sourcecred","github","AUTHORS","5","sourcecred","github","USERLIKE","USER","decentralion","8","sourcecred","github","COMMENT","ISSUE","sourcecred","example-github","6","385223316"],"dstIndex":20,"srcIndex":48},{"address":["sourcecred","github","AUTHORS","5","sourcecred","github","USERLIKE","USER","decentralion","8","sourcecred","github","COMMENT","PULL","sourcecred","example-github","3","369162222"],"dstIndex":22,"srcIndex":48},{"address":["sourcecred","github","AUTHORS","5","sourcecred","github","USERLIKE","USER","wchargin","6","sourcecred","github","ISSUE","sourcecred","example-github","10"],"dstIndex":32,"srcIndex":49},{"address":["sourcecred","github","AUTHORS","5","sourcecred","github","USERLIKE","USER","wchargin","6","sourcecred","github","ISSUE","sourcecred","example-github","11"],"dstIndex":33,"srcIndex":49},{"address":["sourcecred","github","AUTHORS","5","sourcecred","github","USERLIKE","USER","wchargin","6","sourcecred","github","PULL","sourcecred","example-github","9"],"dstIndex":43,"srcIndex":49},{"address":["sourcecred","github","AUTHORS","5","sourcecred","github","USERLIKE","USER","wchargin","7","sourcecred","github","REVIEW","sourcecred","example-github","5","100313899"],"dstIndex":45,"srcIndex":49},{"address":["sourcecred","github","AUTHORS","5","sourcecred","github","USERLIKE","USER","wchargin","7","sourcecred","github","REVIEW","sourcecred","example-github","5","100314038"],"dstIndex":46,"srcIndex":49},{"address":["sourcecred","github","AUTHORS","5","sourcecred","github","USERLIKE","USER","wchargin","8","sourcecred","github","COMMENT","ISSUE","sourcecred","example-github","11","420813013"],"dstIndex":7,"srcIndex":49},{"address":["sourcecred","github","AUTHORS","5","sourcecred","github","USERLIKE","USER","wchargin","8","sourcecred","github","COMMENT","PULL","sourcecred","example-github","5","396430464"],"dstIndex":23,"srcIndex":49},{"address":["sourcecred","github","AUTHORS","5","sourcecred","github","USERLIKE","USER","wchargin","9","sourcecred","github","COMMENT","REVIEW","sourcecred","example-github","5","100313899","171460198"],"dstIndex":24,"srcIndex":49},{"address":["sourcecred","github","CORRESPONDS_TO_COMMIT_TYPE","4","sourcecred","github","COMMIT","MDY6Q29tbWl0MTIzMjU1MDA2OjBhMjIzMzQ2YjRlNmRlYzAxMjdiMWU2YWE4OTJjNGVlMDQyNGI2NmE="],"dstIndex":0,"srcIndex":25},{"address":["sourcecred","github","CORRESPONDS_TO_COMMIT_TYPE","4","sourcecred","github","COMMIT","MDY6Q29tbWl0MTIzMjU1MDA2OjZiZDFiNGMwYjcxOWMyMmM2ODhhNzQ4NjNiZTA3YTY5OWI3YjliMzQ="],"dstIndex":1,"srcIndex":26},{"address":["sourcecred","github","CORRESPONDS_TO_COMMIT_TYPE","4","sourcecred","github","COMMIT","MDY6Q29tbWl0MTIzMjU1MDA2OjZkNWIzYWEzMWViYjY4YTA2Y2ViNDZiYmQ2Y2Y0OWI2Y2NkNmY1ZTY="],"dstIndex":2,"srcIndex":27},{"address":["sourcecred","github","CORRESPONDS_TO_COMMIT_TYPE","4","sourcecred","github","COMMIT","MDY6Q29tbWl0MTIzMjU1MDA2OmM0MzBiZDc0NDU1MTA1Zjc3MjE1ZWNlNTE5NDUwOTRjZWVlZTZjODY="],"dstIndex":3,"srcIndex":28},{"address":["sourcecred","github","CORRESPONDS_TO_COMMIT_TYPE","4","sourcecred","github","COMMIT","MDY6Q29tbWl0MTIzMjU1MDA2OmVjOTFhZGI3MThhNjA0NWI0OTIzMDNmMDBkOGU4YmViOTU3ZGM3ODA="],"dstIndex":4,"srcIndex":29},{"address":["sourcecred","github","CORRESPONDS_TO_COMMIT_TYPE","4","sourcecred","github","COMMIT","MDY6Q29tbWl0MTIzMjU1MDA2OmVjYzg4OWRjOTRjZjZkYTE3YWU2ZWFiNWJiN2I3MTU1ZjU3NzUxOWQ="],"dstIndex":5,"srcIndex":30},{"address":["sourcecred","github","HAS_PARENT","6","sourcecred","github","ISSUE","sourcecred","example-github","1"],"dstIndex":44,"srcIndex":31},{"address":["sourcecred","github","HAS_PARENT","6","sourcecred","github","ISSUE","sourcecred","example-github","10"],"dstIndex":44,"srcIndex":32},{"address":["sourcecred","github","HAS_PARENT","6","sourcecred","github","ISSUE","sourcecred","example-github","11"],"dstIndex":44,"srcIndex":33},{"address":["sourcecred","github","HAS_PARENT","6","sourcecred","github","ISSUE","sourcecred","example-github","12"],"dstIndex":44,"srcIndex":34},{"address":["sourcecred","github","HAS_PARENT","6","sourcecred","github","ISSUE","sourcecred","example-github","13"],"dstIndex":44,"srcIndex":35},{"address":["sourcecred","github","HAS_PARENT","6","sourcecred","github","ISSUE","sourcecred","example-github","2"],"dstIndex":44,"srcIndex":36},{"address":["sourcecred","github","HAS_PARENT","6","sourcecred","github","ISSUE","sourcecred","example-github","4"],"dstIndex":44,"srcIndex":37},{"address":["sourcecred","github","HAS_PARENT","6","sourcecred","github","ISSUE","sourcecred","example-github","6"],"dstIndex":44,"srcIndex":38},{"address":["sourcecred","github","HAS_PARENT","6","sourcecred","github","ISSUE","sourcecred","example-github","7"],"dstIndex":44,"srcIndex":39},{"address":["sourcecred","github","HAS_PARENT","6","sourcecred","github","ISSUE","sourcecred","example-github","8"],"dstIndex":44,"srcIndex":40},{"address":["sourcecred","github","HAS_PARENT","6","sourcecred","github","PULL","sourcecred","example-github","3"],"dstIndex":44,"srcIndex":41},{"address":["sourcecred","github","HAS_PARENT","6","sourcecred","github","PULL","sourcecred","example-github","5"],"dstIndex":44,"srcIndex":42},{"address":["sourcecred","github","HAS_PARENT","6","sourcecred","github","PULL","sourcecred","example-github","9"],"dstIndex":44,"srcIndex":43},{"address":["sourcecred","github","HAS_PARENT","7","sourcecred","github","REVIEW","sourcecred","example-github","5","100313899"],"dstIndex":42,"srcIndex":45},{"address":["sourcecred","github","HAS_PARENT","7","sourcecred","github","REVIEW","sourcecred","example-github","5","100314038"],"dstIndex":42,"srcIndex":46},{"address":["sourcecred","github","HAS_PARENT","8","sourcecred","github","COMMENT","ISSUE","sourcecred","example-github","11","420811872"],"dstIndex":33,"srcIndex":6},{"address":["sourcecred","github","HAS_PARENT","8","sourcecred","github","COMMENT","ISSUE","sourcecred","example-github","11","420813013"],"dstIndex":33,"srcIndex":7},{"address":["sourcecred","github","HAS_PARENT","8","sourcecred","github","COMMENT","ISSUE","sourcecred","example-github","11","420813206"],"dstIndex":33,"srcIndex":8},{"address":["sourcecred","github","HAS_PARENT","8","sourcecred","github","COMMENT","ISSUE","sourcecred","example-github","11","420813621"],"dstIndex":33,"srcIndex":9},{"address":["sourcecred","github","HAS_PARENT","8","sourcecred","github","COMMENT","ISSUE","sourcecred","example-github","2","373768703"],"dstIndex":36,"srcIndex":10},{"address":["sourcecred","github","HAS_PARENT","8","sourcecred","github","COMMENT","ISSUE","sourcecred","example-github","2","373768850"],"dstIndex":36,"srcIndex":11},{"address":["sourcecred","github","HAS_PARENT","8","sourcecred","github","COMMENT","ISSUE","sourcecred","example-github","2","385576185"],"dstIndex":36,"srcIndex":12},{"address":["sourcecred","github","HAS_PARENT","8","sourcecred","github","COMMENT","ISSUE","sourcecred","example-github","2","385576220"],"dstIndex":36,"srcIndex":13},{"address":["sourcecred","github","HAS_PARENT","8","sourcecred","github","COMMENT","ISSUE","sourcecred","example-github","2","385576248"],"dstIndex":36,"srcIndex":14},{"address":["sourcecred","github","HAS_PARENT","8","sourcecred","github","COMMENT","ISSUE","sourcecred","example-github","2","385576273"],"dstIndex":36,"srcIndex":15},{"address":["sourcecred","github","HAS_PARENT","8","sourcecred","github","COMMENT","ISSUE","sourcecred","example-github","2","385576920"],"dstIndex":36,"srcIndex":16},{"address":["sourcecred","github","HAS_PARENT","8","sourcecred","github","COMMENT","ISSUE","sourcecred","example-github","2","385576936"],"dstIndex":36,"srcIndex":17},{"address":["sourcecred","github","HAS_PARENT","8","sourcecred","github","COMMENT","ISSUE","sourcecred","example-github","6","373768442"],"dstIndex":38,"srcIndex":18},{"address":["sourcecred","github","HAS_PARENT","8","sourcecred","github","COMMENT","ISSUE","sourcecred","example-github","6","373768538"],"dstIndex":38,"srcIndex":19},{"address":["sourcecred","github","HAS_PARENT","8","sourcecred","github","COMMENT","ISSUE","sourcecred","example-github","6","385223316"],"dstIndex":38,"srcIndex":20},{"address":["sourcecred","github","HAS_PARENT","8","sourcecred","github","COMMENT","ISSUE","sourcecred","example-github","6","417104047"],"dstIndex":38,"srcIndex":21},{"address":["sourcecred","github","HAS_PARENT","8","sourcecred","github","COMMENT","PULL","sourcecred","example-github","3","369162222"],"dstIndex":41,"srcIndex":22},{"address":["sourcecred","github","HAS_PARENT","8","sourcecred","github","COMMENT","PULL","sourcecred","example-github","5","396430464"],"dstIndex":42,"srcIndex":23},{"address":["sourcecred","github","HAS_PARENT","9","sourcecred","github","COMMENT","REVIEW","sourcecred","example-github","5","100313899","171460198"],"dstIndex":45,"srcIndex":24},{"address":["sourcecred","github","MERGED_AS","6","sourcecred","github","PULL","sourcecred","example-github","3"],"dstIndex":25,"srcIndex":41},{"address":["sourcecred","github","MERGED_AS","6","sourcecred","github","PULL","sourcecred","example-github","5"],"dstIndex":27,"srcIndex":42},{"address":["sourcecred","github","REACTS","HEART","5","sourcecred","github","USERLIKE","USER","decentralion","6","sourcecred","github","ISSUE","sourcecred","example-github","1"],"dstIndex":31,"srcIndex":48},{"address":["sourcecred","github","REACTS","HEART","5","sourcecred","github","USERLIKE","USER","decentralion","6","sourcecred","github","ISSUE","sourcecred","example-github","13"],"dstIndex":35,"srcIndex":48},{"address":["sourcecred","github","REACTS","HEART","5","sourcecred","github","USERLIKE","USER","decentralion","6","sourcecred","github","PULL","sourcecred","example-github","9"],"dstIndex":43,"srcIndex":48},{"address":["sourcecred","github","REACTS","HEART","5","sourcecred","github","USERLIKE","USER","decentralion","8","sourcecred","github","COMMENT","PULL","sourcecred","example-github","5","396430464"],"dstIndex":23,"srcIndex":48},{"address":["sourcecred","github","REACTS","HEART","5","sourcecred","github","USERLIKE","USER","decentralion","9","sourcecred","github","COMMENT","REVIEW","sourcecred","example-github","5","100313899","171460198"],"dstIndex":24,"srcIndex":48},{"address":["sourcecred","github","REACTS","HEART","5","sourcecred","github","USERLIKE","USER","wchargin","9","sourcecred","github","COMMENT","REVIEW","sourcecred","example-github","5","100313899","171460198"],"dstIndex":24,"srcIndex":49},{"address":["sourcecred","github","REACTS","HOORAY","5","sourcecred","github","USERLIKE","USER","decentralion","6","sourcecred","github","ISSUE","sourcecred","example-github","13"],"dstIndex":35,"srcIndex":48},{"address":["sourcecred","github","REACTS","HOORAY","5","sourcecred","github","USERLIKE","USER","decentralion","8","sourcecred","github","COMMENT","ISSUE","sourcecred","example-github","11","420813206"],"dstIndex":8,"srcIndex":48},{"address":["sourcecred","github","REACTS","HOORAY","5","sourcecred","github","USERLIKE","USER","decentralion","9","sourcecred","github","COMMENT","REVIEW","sourcecred","example-github","5","100313899","171460198"],"dstIndex":24,"srcIndex":48},{"address":["sourcecred","github","REACTS","HOORAY","5","sourcecred","github","USERLIKE","USER","wchargin","9","sourcecred","github","COMMENT","REVIEW","sourcecred","example-github","5","100313899","171460198"],"dstIndex":24,"srcIndex":49},{"address":["sourcecred","github","REACTS","ROCKET","5","sourcecred","github","USERLIKE","USER","decentralion","6","sourcecred","github","ISSUE","sourcecred","example-github","13"],"dstIndex":35,"srcIndex":48},{"address":["sourcecred","github","REACTS","THUMBS_UP","5","sourcecred","github","USERLIKE","USER","decentralion","6","sourcecred","github","ISSUE","sourcecred","example-github","12"],"dstIndex":34,"srcIndex":48},{"address":["sourcecred","github","REACTS","THUMBS_UP","5","sourcecred","github","USERLIKE","USER","decentralion","6","sourcecred","github","ISSUE","sourcecred","example-github","13"],"dstIndex":35,"srcIndex":48},{"address":["sourcecred","github","REACTS","THUMBS_UP","5","sourcecred","github","USERLIKE","USER","decentralion","6","sourcecred","github","PULL","sourcecred","example-github","9"],"dstIndex":43,"srcIndex":48},{"address":["sourcecred","github","REACTS","THUMBS_UP","5","sourcecred","github","USERLIKE","USER","decentralion","8","sourcecred","github","COMMENT","ISSUE","sourcecred","example-github","11","420813206"],"dstIndex":8,"srcIndex":48},{"address":["sourcecred","github","REACTS","THUMBS_UP","5","sourcecred","github","USERLIKE","USER","decentralion","9","sourcecred","github","COMMENT","REVIEW","sourcecred","example-github","5","100313899","171460198"],"dstIndex":24,"srcIndex":48},{"address":["sourcecred","github","REACTS","THUMBS_UP","5","sourcecred","github","USERLIKE","USER","wchargin","9","sourcecred","github","COMMENT","REVIEW","sourcecred","example-github","5","100313899","171460198"],"dstIndex":24,"srcIndex":49},{"address":["sourcecred","github","REFERENCES","4","sourcecred","github","COMMIT","MDY6Q29tbWl0MTIzMjU1MDA2OjBhMjIzMzQ2YjRlNmRlYzAxMjdiMWU2YWE4OTJjNGVlMDQyNGI2NmE=","6","sourcecred","github","PULL","sourcecred","example-github","3"],"dstIndex":41,"srcIndex":25},{"address":["sourcecred","github","REFERENCES","4","sourcecred","github","COMMIT","MDY6Q29tbWl0MTIzMjU1MDA2OjZiZDFiNGMwYjcxOWMyMmM2ODhhNzQ4NjNiZTA3YTY5OWI3YjliMzQ=","5","sourcecred","github","USERLIKE","USER","wchargin"],"dstIndex":49,"srcIndex":26},{"address":["sourcecred","github","REFERENCES","4","sourcecred","github","COMMIT","MDY6Q29tbWl0MTIzMjU1MDA2OjZkNWIzYWEzMWViYjY4YTA2Y2ViNDZiYmQ2Y2Y0OWI2Y2NkNmY1ZTY=","6","sourcecred","github","PULL","sourcecred","example-github","5"],"dstIndex":42,"srcIndex":27},{"address":["sourcecred","github","REFERENCES","4","sourcecred","github","COMMIT","MDY6Q29tbWl0MTIzMjU1MDA2OmM0MzBiZDc0NDU1MTA1Zjc3MjE1ZWNlNTE5NDUwOTRjZWVlZTZjODY=","5","sourcecred","github","USERLIKE","USER","wchargin"],"dstIndex":49,"srcIndex":28},{"address":["sourcecred","github","REFERENCES","6","sourcecred","github","ISSUE","sourcecred","example-github","10","6","sourcecred","github","ISSUE","sourcecred","example-github","10"],"dstIndex":32,"srcIndex":32},{"address":["sourcecred","github","REFERENCES","6","sourcecred","github","ISSUE","sourcecred","example-github","10","6","sourcecred","github","ISSUE","sourcecred","example-github","2"],"dstIndex":36,"srcIndex":32},{"address":["sourcecred","github","REFERENCES","6","sourcecred","github","ISSUE","sourcecred","example-github","12","4","sourcecred","github","COMMIT","MDY6Q29tbWl0MTIzMjU1MDA2OmVjOTFhZGI3MThhNjA0NWI0OTIzMDNmMDBkOGU4YmViOTU3ZGM3ODA="],"dstIndex":29,"srcIndex":34},{"address":["sourcecred","github","REFERENCES","6","sourcecred","github","ISSUE","sourcecred","example-github","12","4","sourcecred","github","COMMIT","MDY6Q29tbWl0MTIzMjU1MDA2OmVjYzg4OWRjOTRjZjZkYTE3YWU2ZWFiNWJiN2I3MTU1ZjU3NzUxOWQ="],"dstIndex":30,"srcIndex":34},{"address":["sourcecred","github","REFERENCES","6","sourcecred","github","ISSUE","sourcecred","example-github","2","6","sourcecred","github","ISSUE","sourcecred","example-github","1"],"dstIndex":31,"srcIndex":36},{"address":["sourcecred","github","REFERENCES","6","sourcecred","github","PULL","sourcecred","example-github","5","5","sourcecred","github","USERLIKE","USER","wchargin"],"dstIndex":49,"srcIndex":42},{"address":["sourcecred","github","REFERENCES","8","sourcecred","github","COMMENT","ISSUE","sourcecred","example-github","2","373768703","6","sourcecred","github","ISSUE","sourcecred","example-github","6"],"dstIndex":38,"srcIndex":10},{"address":["sourcecred","github","REFERENCES","8","sourcecred","github","COMMENT","ISSUE","sourcecred","example-github","2","373768850","8","sourcecred","github","COMMENT","ISSUE","sourcecred","example-github","6","373768538"],"dstIndex":19,"srcIndex":11},{"address":["sourcecred","github","REFERENCES","8","sourcecred","github","COMMENT","ISSUE","sourcecred","example-github","2","385576185","6","sourcecred","github","PULL","sourcecred","example-github","5"],"dstIndex":42,"srcIndex":12},{"address":["sourcecred","github","REFERENCES","8","sourcecred","github","COMMENT","ISSUE","sourcecred","example-github","2","385576220","7","sourcecred","github","REVIEW","sourcecred","example-github","5","100313899"],"dstIndex":45,"srcIndex":13},{"address":["sourcecred","github","REFERENCES","8","sourcecred","github","COMMENT","ISSUE","sourcecred","example-github","2","385576248","9","sourcecred","github","COMMENT","REVIEW","sourcecred","example-github","5","100313899","171460198"],"dstIndex":24,"srcIndex":14},{"address":["sourcecred","github","REFERENCES","8","sourcecred","github","COMMENT","ISSUE","sourcecred","example-github","2","385576273","5","sourcecred","github","USERLIKE","USER","wchargin"],"dstIndex":49,"srcIndex":15},{"address":["sourcecred","github","REFERENCES","8","sourcecred","github","COMMENT","ISSUE","sourcecred","example-github","2","385576920","6","sourcecred","github","ISSUE","sourcecred","example-github","1"],"dstIndex":31,"srcIndex":16},{"address":["sourcecred","github","REFERENCES","8","sourcecred","github","COMMENT","ISSUE","sourcecred","example-github","2","385576920","6","sourcecred","github","ISSUE","sourcecred","example-github","2"],"dstIndex":36,"srcIndex":16},{"address":["sourcecred","github","REFERENCES","8","sourcecred","github","COMMENT","ISSUE","sourcecred","example-github","2","385576920","6","sourcecred","github","PULL","sourcecred","example-github","3"],"dstIndex":41,"srcIndex":16},{"address":["sourcecred","github","REFERENCES","8","sourcecred","github","COMMENT","ISSUE","sourcecred","example-github","2","385576920","7","sourcecred","github","REVIEW","sourcecred","example-github","5","100313899"],"dstIndex":45,"srcIndex":16},{"address":["sourcecred","github","REFERENCES","8","sourcecred","github","COMMENT","ISSUE","sourcecred","example-github","2","385576920","9","sourcecred","github","COMMENT","REVIEW","sourcecred","example-github","5","100313899","171460198"],"dstIndex":24,"srcIndex":16},{"address":["sourcecred","github","REFERENCES","8","sourcecred","github","COMMENT","ISSUE","sourcecred","example-github","6","385223316","6","sourcecred","github","ISSUE","sourcecred","example-github","2"],"dstIndex":36,"srcIndex":20},{"address":["sourcecred","github","REFERENCES","8","sourcecred","github","COMMENT","PULL","sourcecred","example-github","3","369162222","6","sourcecred","github","ISSUE","sourcecred","example-github","2"],"dstIndex":36,"srcIndex":22}],"nodes":[{"index":0},{"index":1},{"index":2},{"index":3},{"index":4},{"index":5},{"index":6},{"index":7},{"index":8},{"index":9},{"index":10},{"index":11},{"index":12},{"index":13},{"index":14},{"index":15},{"index":16},{"index":17},{"index":18},{"index":19},{"index":20},{"index":21},{"index":22},{"index":23},{"index":24},{"index":25},{"index":26},{"index":27},{"index":28},{"index":29},{"index":30},{"index":31},{"index":32},{"index":33},{"index":34},{"index":35},{"index":36},{"index":37},{"index":38},{"index":39},{"index":40},{"index":41},{"index":42},{"index":43},{"index":44},{"index":45},{"index":46},{"index":47},{"index":48},{"index":49}],"sortedNodeAddresses":[["sourcecred","git","COMMIT","0a223346b4e6dec0127b1e6aa892c4ee0424b66a"],["sourcecred","git","COMMIT","6bd1b4c0b719c22c688a74863be07a699b7b9b34"],["sourcecred","git","COMMIT","6d5b3aa31ebb68a06ceb46bbd6cf49b6ccd6f5e6"],["sourcecred","git","COMMIT","c430bd74455105f77215ece51945094ceeee6c86"],["sourcecred","git","COMMIT","ec91adb718a6045b492303f00d8e8beb957dc780"],["sourcecred","git","COMMIT","ecc889dc94cf6da17ae6eab5bb7b7155f577519d"],["sourcecred","github","COMMENT","ISSUE","sourcecred","example-github","11","420811872"],["sourcecred","github","COMMENT","ISSUE","sourcecred","example-github","11","420813013"],["sourcecred","github","COMMENT","ISSUE","sourcecred","example-github","11","420813206"],["sourcecred","github","COMMENT","ISSUE","sourcecred","example-github","11","420813621"],["sourcecred","github","COMMENT","ISSUE","sourcecred","example-github","2","373768703"],["sourcecred","github","COMMENT","ISSUE","sourcecred","example-github","2","373768850"],["sourcecred","github","COMMENT","ISSUE","sourcecred","example-github","2","385576185"],["sourcecred","github","COMMENT","ISSUE","sourcecred","example-github","2","385576220"],["sourcecred","github","COMMENT","ISSUE","sourcecred","example-github","2","385576248"],["sourcecred","github","COMMENT","ISSUE","sourcecred","example-github","2","385576273"],["sourcecred","github","COMMENT","ISSUE","sourcecred","example-github","2","385576920"],["sourcecred","github","COMMENT","ISSUE","sourcecred","example-github","2","385576936"],["sourcecred","github","COMMENT","ISSUE","sourcecred","example-github","6","373768442"],["sourcecred","github","COMMENT","ISSUE","sourcecred","example-github","6","373768538"],["sourcecred","github","COMMENT","ISSUE","sourcecred","example-github","6","385223316"],["sourcecred","github","COMMENT","ISSUE","sourcecred","example-github","6","417104047"],["sourcecred","github","COMMENT","PULL","sourcecred","example-github","3","369162222"],["sourcecred","github","COMMENT","PULL","sourcecred","example-github","5","396430464"],["sourcecred","github","COMMENT","REVIEW","sourcecred","example-github","5","100313899","171460198"],["sourcecred","github","COMMIT","MDY6Q29tbWl0MTIzMjU1MDA2OjBhMjIzMzQ2YjRlNmRlYzAxMjdiMWU2YWE4OTJjNGVlMDQyNGI2NmE="],["sourcecred","github","COMMIT","MDY6Q29tbWl0MTIzMjU1MDA2OjZiZDFiNGMwYjcxOWMyMmM2ODhhNzQ4NjNiZTA3YTY5OWI3YjliMzQ="],["sourcecred","github","COMMIT","MDY6Q29tbWl0MTIzMjU1MDA2OjZkNWIzYWEzMWViYjY4YTA2Y2ViNDZiYmQ2Y2Y0OWI2Y2NkNmY1ZTY="],["sourcecred","github","COMMIT","MDY6Q29tbWl0MTIzMjU1MDA2OmM0MzBiZDc0NDU1MTA1Zjc3MjE1ZWNlNTE5NDUwOTRjZWVlZTZjODY="],["sourcecred","github","COMMIT","MDY6Q29tbWl0MTIzMjU1MDA2OmVjOTFhZGI3MThhNjA0NWI0OTIzMDNmMDBkOGU4YmViOTU3ZGM3ODA="],["sourcecred","github","COMMIT","MDY6Q29tbWl0MTIzMjU1MDA2OmVjYzg4OWRjOTRjZjZkYTE3YWU2ZWFiNWJiN2I3MTU1ZjU3NzUxOWQ="],["sourcecred","github","ISSUE","sourcecred","example-github","1"],["sourcecred","github","ISSUE","sourcecred","example-github","10"],["sourcecred","github","ISSUE","sourcecred","example-github","11"],["sourcecred","github","ISSUE","sourcecred","example-github","12"],["sourcecred","github","ISSUE","sourcecred","example-github","13"],["sourcecred","github","ISSUE","sourcecred","example-github","2"],["sourcecred","github","ISSUE","sourcecred","example-github","4"],["sourcecred","github","ISSUE","sourcecred","example-github","6"],["sourcecred","github","ISSUE","sourcecred","example-github","7"],["sourcecred","github","ISSUE","sourcecred","example-github","8"],["sourcecred","github","PULL","sourcecred","example-github","3"],["sourcecred","github","PULL","sourcecred","example-github","5"],["sourcecred","github","PULL","sourcecred","example-github","9"],["sourcecred","github","REPO","sourcecred","example-github"],["sourcecred","github","REVIEW","sourcecred","example-github","5","100313899"],["sourcecred","github","REVIEW","sourcecred","example-github","5","100314038"],["sourcecred","github","USERLIKE","BOT","credbot"],["sourcecred","github","USERLIKE","USER","decentralion"],["sourcecred","github","USERLIKE","USER","wchargin"]]}] \ No newline at end of file diff --git a/src/core/__snapshots__/graph.test.js.snap b/src/core/__snapshots__/graph.test.js.snap index eb8b511..6fa4cef 100644 --- a/src/core/__snapshots__/graph.test.js.snap +++ b/src/core/__snapshots__/graph.test.js.snap @@ -1,10 +1,39 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP +exports[`core/graph toJSON / fromJSON snapshot testing a graph with a dangling edge 1`] = ` +Array [ + Object { + "type": "sourcecred/graph", + "version": "0.5.0", + }, + Object { + "edges": Array [ + Object { + "address": Array [ + "edge", + ], + "dstIndex": 0, + "srcIndex": 1, + }, + ], + "nodes": Array [], + "sortedNodeAddresses": Array [ + Array [ + "dst", + ], + Array [ + "src", + ], + ], + }, +] +`; + exports[`core/graph toJSON / fromJSON snapshot testing a trivial graph 1`] = ` Array [ Object { "type": "sourcecred/graph", - "version": "0.4.0", + "version": "0.5.0", }, Object { "edges": Array [ @@ -24,6 +53,14 @@ Array [ }, ], "nodes": Array [ + Object { + "index": 0, + }, + Object { + "index": 1, + }, + ], + "sortedNodeAddresses": Array [ Array [ "dst", ], @@ -39,17 +76,31 @@ exports[`core/graph toJSON / fromJSON snapshot testing an advanced graph 1`] = ` Array [ Object { "type": "sourcecred/graph", - "version": "0.4.0", + "version": "0.5.0", }, Object { "edges": Array [ + Object { + "address": Array [ + "full-dangling", + ], + "dstIndex": 4, + "srcIndex": 4, + }, + Object { + "address": Array [ + "half-dangling", + ], + "dstIndex": 4, + "srcIndex": 1, + }, Object { "address": Array [ "hom", "1", ], "dstIndex": 0, - "srcIndex": 3, + "srcIndex": 5, }, Object { "address": Array [ @@ -57,26 +108,49 @@ Array [ "2", ], "dstIndex": 0, - "srcIndex": 3, + "srcIndex": 5, }, Object { "address": Array [ "loop", ], - "dstIndex": 2, - "srcIndex": 2, + "dstIndex": 3, + "srcIndex": 3, }, ], "nodes": Array [ + Object { + "index": 0, + }, + Object { + "index": 1, + }, + Object { + "index": 2, + }, + Object { + "index": 3, + }, + Object { + "index": 5, + }, + ], + "sortedNodeAddresses": Array [ Array [ "dst", ], + Array [ + "halfIsolated", + ], Array [ "isolated", ], Array [ "loop", ], + Array [ + "phantom", + ], Array [ "src", ], diff --git a/src/core/__snapshots__/pagerankGraph.test.js.snap b/src/core/__snapshots__/pagerankGraph.test.js.snap index 66d4ce3..ed177c3 100644 --- a/src/core/__snapshots__/pagerankGraph.test.js.snap +++ b/src/core/__snapshots__/pagerankGraph.test.js.snap @@ -15,17 +15,31 @@ Array [ "graphJSON": Array [ Object { "type": "sourcecred/graph", - "version": "0.4.0", + "version": "0.5.0", }, Object { "edges": Array [ + Object { + "address": Array [ + "full-dangling", + ], + "dstIndex": 4, + "srcIndex": 4, + }, + Object { + "address": Array [ + "half-dangling", + ], + "dstIndex": 4, + "srcIndex": 1, + }, Object { "address": Array [ "hom", "1", ], "dstIndex": 0, - "srcIndex": 3, + "srcIndex": 5, }, Object { "address": Array [ @@ -33,26 +47,49 @@ Array [ "2", ], "dstIndex": 0, - "srcIndex": 3, + "srcIndex": 5, }, Object { "address": Array [ "loop", ], - "dstIndex": 2, - "srcIndex": 2, + "dstIndex": 3, + "srcIndex": 3, }, ], "nodes": Array [ + Object { + "index": 0, + }, + Object { + "index": 1, + }, + Object { + "index": 2, + }, + Object { + "index": 3, + }, + Object { + "index": 5, + }, + ], + "sortedNodeAddresses": Array [ Array [ "dst", ], + Array [ + "halfIsolated", + ], Array [ "isolated", ], Array [ "loop", ], + Array [ + "phantom", + ], Array [ "src", ], @@ -60,10 +97,11 @@ Array [ }, ], "scores": Array [ - 0.25, - 0.25, - 0.25, - 0.25, + 0.2, + 0.2, + 0.2, + 0.2, + 0.2, ], "syntheticLoopWeight": 0.001, "toWeights": Array [ diff --git a/src/core/attribution/graphToMarkovChain.js b/src/core/attribution/graphToMarkovChain.js index 05a4803..e047cf3 100644 --- a/src/core/attribution/graphToMarkovChain.js +++ b/src/core/attribution/graphToMarkovChain.js @@ -82,7 +82,7 @@ export function createConnections( } // Process edges. - for (const edge of graph.edges()) { + for (const edge of graph.edges({showDangling: false})) { const {toWeight, froWeight} = edgeWeight(edge); const {src, dst} = edge; processConnection(dst, { diff --git a/src/core/attribution/graphToMarkovChain.test.js b/src/core/attribution/graphToMarkovChain.test.js index d680826..fb97baf 100644 --- a/src/core/attribution/graphToMarkovChain.test.js +++ b/src/core/attribution/graphToMarkovChain.test.js @@ -2,7 +2,7 @@ import sortBy from "lodash.sortby"; -import {Graph} from "../graph"; +import {Graph, EdgeAddress} from "../graph"; import { createConnections, createOrderedSparseMarkovChain, @@ -142,6 +142,28 @@ describe("core/attribution/graphToMarkovChain", () => { expect(normalize(osmc)).toEqual(normalize(expected)); }); + it("ignores dangling edges", () => { + const e1 = { + src: n1.address, + dst: n2.address, + address: EdgeAddress.fromParts(["e1"]), + }; + const g = new Graph().addNode(n1).addEdge(e1); + const edgeWeight = (_unused_edge) => { + throw new Error("Don't even look at me"); + }; + const osmc = createOrderedSparseMarkovChain( + createConnections(g, edgeWeight, 1e-3) + ); + const expected = { + nodeOrder: [n1.address], + chain: [ + {neighbor: new Uint32Array([0]), weight: new Float64Array([1.0])}, + ], + }; + expect(normalize(osmc)).toEqual(normalize(expected)); + }); + it("works on a simple asymmetric chain", () => { const e1 = edge("e1", n1, n2); const e2 = edge("e2", n2, n3); @@ -248,6 +270,7 @@ describe("core/attribution/graphToMarkovChain", () => { ag.nodes.dst.address, ag.nodes.loop.address, ag.nodes.isolated.address, + ag.nodes.halfIsolated.address, ], chain: [ { @@ -260,6 +283,7 @@ describe("core/attribution/graphToMarkovChain", () => { }, {neighbor: new Uint32Array([2]), weight: new Float64Array([1])}, {neighbor: new Uint32Array([3]), weight: new Float64Array([1])}, + {neighbor: new Uint32Array([4]), weight: new Float64Array([1])}, ], }; expect(normalize(osmc)).toEqual(normalize(expected)); diff --git a/src/core/graph.js b/src/core/graph.js index b1d9b3d..4a6d15b 100644 --- a/src/core/graph.js +++ b/src/core/graph.js @@ -63,6 +63,16 @@ import * as NullUtil from "../util/null"; * edge respectively, and both fields contain `NodeAddressT`s. The edge also * has its own address, which is an `EdgeAddressT`. * + * Graphs are allowed to contain Edges whose `src` or `dst` are not present. + * Such edges are called 'Dangling Edges'. An edge may convert from dangling to + * non-dangling (if it is added before its src or dst), and it may convert from + * non-dangling to dangling (if its src or dst are removed). + * + * Supporting dangling edges is important, because it means that we can require + * metadata be present for a Node (e.g. its creation timestamp), and still + * allow graph creators that do not know a node's metadata to create references + * to it. (Of course, they still need to know the node's address). + * * Here's a toy example of creating a graph: * * ```js @@ -84,6 +94,7 @@ import * as NullUtil from "../util/null"; * - `node` to retrieve a node by its address * - `nodes` to iterate over the nodes in the graph * - `hasEdge` to check if an edge address is in the Graph + * - `isDanglingEdge` to check if an edge is dangling * - `edge` to retrieve an edge by its address * - `edges` to iterate over the edges in the graph * - `neighbors` to find all the edges and nodes adjacent to a node @@ -121,7 +132,7 @@ export type Edge = {| +dst: NodeAddressT, |}; -const COMPAT_INFO = {type: "sourcecred/graph", version: "0.4.0"}; +const COMPAT_INFO = {type: "sourcecred/graph", version: "0.5.0"}; export type Neighbor = {|+node: Node, +edge: Edge|}; @@ -143,13 +154,23 @@ export type NeighborsOptions = {| |}; export type EdgesOptions = {| - +addressPrefix: EdgeAddressT, - +srcPrefix: NodeAddressT, - +dstPrefix: NodeAddressT, + // An edge address prefix. Only show edges whose addresses match this prefix. + +addressPrefix?: EdgeAddressT, + // A node address prefix. Only show edges whose src matches + // this prefix. + +srcPrefix?: NodeAddressT, + // A node address prefix. Only show edges whose dst matches + // this prefix. + +dstPrefix?: NodeAddressT, + // Determines whether dangling edges should be included in the results. + +showDangling: boolean, |}; type AddressJSON = string[]; // Result of calling {Node,Edge}Address.toParts type Integer = number; +type IndexedNodeJSON = {| + +index: Integer, +|}; type IndexedEdgeJSON = {| +address: AddressJSON, +srcIndex: Integer, @@ -157,7 +178,10 @@ type IndexedEdgeJSON = {| |}; export opaque type GraphJSON = Compatible<{| - +nodes: AddressJSON[], + // A node address can be present because it corresponds to a node, or because + // it is referenced by a dangling edge. + +sortedNodeAddresses: AddressJSON[], + +nodes: IndexedNodeJSON[], +edges: IndexedEdgeJSON[], |}>; @@ -217,6 +241,41 @@ export class Graph { this._maybeCheckInvariants(); } + /** + * A node address is 'referenced' if it is either present in the graph, or is + * the src or dst of some edge. + * + * Referenced nodes always have an entry in this._incidentEdges (regardless + * of whether they are incident to any edges). + * + * This method ensures that a given node address has a reference. + */ + _reference(n: NodeAddressT) { + if (!this._incidentEdges.has(n)) { + this._incidentEdges.set(n, {inEdges: [], outEdges: []}); + } + } + + /** + * A node stops being referenced as soon as it is both not in the graph, and is + * not incident to any edge. This method must be called after any operation which + * might cause a node address to no longer be referenced, so that the node can + * be unreferenced if appropriate. + */ + _unreference(n: NodeAddressT) { + const incidence = this._incidentEdges.get(n); + if (incidence != null) { + const {inEdges, outEdges} = incidence; + if ( + !this._nodes.has(n) && + inEdges.length === 0 && + outEdges.length === 0 + ) { + this._incidentEdges.delete(n); + } + } + } + /** * Returns how many times the graph has been modified. * @@ -248,10 +307,10 @@ export class Graph { addNode(node: Node): this { const {address} = node; NodeAddress.assertValid(address); + this._reference(address); const existingNode = this._nodes.get(address); if (existingNode == null) { this._nodes.set(address, node); - this._incidentEdges.set(address, {inEdges: [], outEdges: []}); } else { if (!deepEqual(node, existingNode)) { const strNode = nodeToString(node); @@ -272,37 +331,26 @@ export class Graph { * If the node does not exist in the graph, no action is taken and no error * is thrown. (This operation is idempotent.) * - * If the node is incident to any edges, those edges must be removed - * before removing the node. Attempting to remove a node that is incident - * to some edges will throw an error. + * Removing a node which is incident to some edges is allowed; such edges will + * become dangling edges. See the discussion of 'Dangling Edges' in the module docstring + * for details. * * Returns `this` for chaining. */ removeNode(a: NodeAddressT): this { NodeAddress.assertValid(a); - const {inEdges, outEdges} = this._incidentEdges.get(a) || { - inEdges: [], - outEdges: [], - }; - const existingEdges = inEdges.concat(outEdges); - if (existingEdges.length > 0) { - const strAddress = NodeAddress.toString(a); - const strExampleEdge = edgeToString(existingEdges[0]); - throw new Error( - `Attempted to remove ${strAddress}, which is incident to ${ - existingEdges.length - } edge(s), e.g.: ${strExampleEdge}` - ); - } - this._incidentEdges.delete(a); this._nodes.delete(a); + this._unreference(a); this._markModification(); this._maybeCheckInvariants(); return this; } /** - * Test whether a given node is present in the graph. + * Test whether there exists a Node corresponding to the given NodeAddress. + * + * This will return false for node addresses which are referenced by some + * edge, but not actually present in the graph. */ hasNode(a: NodeAddressT): boolean { NodeAddress.assertValid(a); @@ -363,8 +411,8 @@ export class Graph { /** * Add an edge to the graph. * - * It is an error to add an edge whose source and destination nodes - * are not already present in the graph. + * It is permitted to add an edge if its src or dst are not in the graph. See + * the discussion of 'Dangling Edges' in the module docstring for semantics. * * It is an error to add an edge if a distinct edge with the same address * already exists in the graph (i.e., if the source or destination are @@ -380,12 +428,8 @@ export class Graph { NodeAddress.assertValid(edge.dst, "edge.dst"); EdgeAddress.assertValid(edge.address, "edge.address"); - const srcMissing = !this._nodes.has(edge.src); - const dstMissing = !this._nodes.has(edge.dst); - if (srcMissing || dstMissing) { - const missingThing = srcMissing ? "src" : "dst"; - throw new Error(`Missing ${missingThing} on edge: ${edgeToString(edge)}`); - } + this._reference(edge.src); + this._reference(edge.dst); const existingEdge = this._edges.get(edge.address); if (existingEdge != null) { if ( @@ -440,6 +484,8 @@ export class Graph { } edges.splice(index, 1); }); + this._unreference(edge.src); + this._unreference(edge.dst); } this._markModification(); this._maybeCheckInvariants(); @@ -456,6 +502,27 @@ export class Graph { return result; } + /** + * Test whether there is a dangling edge at the given address. + * + * Returns true if the edge is present, and is dangling. + * Returns false if the edge is present, and is not dangling. + * Returns undefined if the edge is not present. + * + * See the module docstring for more details on dangling edges. + */ + isDanglingEdge(address: EdgeAddressT): boolean | typeof undefined { + EdgeAddress.assertValid(address); + const edge = this.edge(address); + let result: boolean | typeof undefined; + if (edge != null) { + const {src, dst} = edge; + result = !this.hasNode(src) || !this.hasNode(dst); + } + this._maybeCheckInvariants(); + return result; + } + /** * Returns the Edge matching a given EdgeAddressT, if such an edge exists, or * null otherwise. @@ -471,58 +538,65 @@ export class Graph { * Returns an iterator over edges in the graph, optionally filtered by edge * address prefix, source address prefix, and/or destination address prefix. * - * The caller may pass optional arguments to filter by the - * address prefixes for the edge address, the edge src, or the edge dst. + * The caller must pass an options object with a boolean field `showDangling`, + * which determines whether dangling edges will be included in the results. + * The caller may also pass fields `addressPrefix`, `srcPrefix`, and `dstPrefix` + * to perform prefix-based address filtering of edges that are returned. + * (See the module docstring for more context on dangling edges.) * * Suppose that you want to find every edge that represents authorship by a * user. If all authorship edges have the `AUTHORS_EDGE_PREFIX` prefix, and * all user nodes have the `USER_NODE_PREFIX` prefix, then you could call: * * graph.edges({ + * showDangling: true, // or false, irrelevant for this example * addressPrefix: AUTHORS_EDGE_PREFIX, * srcPrefix: USER_NODE_PREFIX, - * dstPrefix: NodeAddress.empty, * }); * - * Note that `NodeAddress.empty` is a prefix of every node address. + * In this example, as `dstPrefix` was left unset, it will default to + * `NodeAddress.empty`, which is a prefix of every node address. * * Clients must not modify the graph during iteration. If they do so, an * error may be thrown at the iteration call site. The iteration order is * undefined. */ - edges(options?: EdgesOptions): Iterator { + edges(options: EdgesOptions): Iterator { if (options == null) { - options = { - addressPrefix: EdgeAddress.empty, - srcPrefix: NodeAddress.empty, - dstPrefix: NodeAddress.empty, - }; + throw new Error("Options are required for Graph.edges"); } - if (options.addressPrefix == null) { - throw new Error( - `Invalid address prefix: ${String(options.addressPrefix)}` - ); - } - if (options.srcPrefix == null) { - throw new Error(`Invalid src prefix: ${String(options.srcPrefix)}`); - } - if (options.dstPrefix == null) { - throw new Error(`Invalid dst prefix: ${String(options.dstPrefix)}`); - } - const result = this._edgesIterator(this._modificationCount, options); + const {showDangling} = options; + const addressPrefix = NullUtil.orElse( + options.addressPrefix, + EdgeAddress.empty + ); + const srcPrefix = NullUtil.orElse(options.srcPrefix, NodeAddress.empty); + const dstPrefix = NullUtil.orElse(options.dstPrefix, NodeAddress.empty); + + const result = this._edgesIterator( + this._modificationCount, + showDangling, + addressPrefix, + srcPrefix, + dstPrefix + ); this._maybeCheckInvariants(); return result; } *_edgesIterator( initialModificationCount: ModificationCount, - options: EdgesOptions + showDangling: boolean, + addressPrefix: EdgeAddressT, + srcPrefix: NodeAddressT, + dstPrefix: NodeAddressT ): Iterator { for (const edge of this._edges.values()) { if ( - EdgeAddress.hasPrefix(edge.address, options.addressPrefix) && - NodeAddress.hasPrefix(edge.src, options.srcPrefix) && - NodeAddress.hasPrefix(edge.dst, options.dstPrefix) + (showDangling || this.isDanglingEdge(edge.address) === false) && + EdgeAddress.hasPrefix(edge.address, addressPrefix) && + NodeAddress.hasPrefix(edge.src, srcPrefix) && + NodeAddress.hasPrefix(edge.dst, dstPrefix) ) { this._checkForComodification(initialModificationCount); this._maybeCheckInvariants(); @@ -543,9 +617,9 @@ export class Graph { * convenience, a `Neighbor` is thus an object that includes both the edge * and the adjacent node. * - * Every edge incident to the root corresponds to exactly one neighbor, but - * note that multiple neighbors may have the same `node` in the case that - * there are multiple edges with the same source and destination. + * Every non-dangling edge incident to the root corresponds to exactly one + * neighbor, but note that multiple neighbors may have the same `node` in the + * case that there are multiple edges with the same source and destination. * * Callers to `neighbors` must provide `NeighborsOptions` as follows: * @@ -568,6 +642,9 @@ export class Graph { * destination (a loop edge), there will be one `Neighbor` with the root node * and the loop edge. * + * No `Neighbors` will be created for dangling edges, as such edges do not + * correspond to any Node in the graph. + * * Clients must not modify the graph during iteration. If they do so, an * error may be thrown at the iteration call site. The iteration order is * undefined. @@ -663,22 +740,33 @@ export class Graph { * the number of edges. */ toJSON(): GraphJSON { - const sortedNodeAddresses = Array.from(this.nodes()) - .map((x) => x.address) - .sort(); - const nodeToSortedIndex = new Map(); + const sortedNodeAddresses = Array.from(this._incidentEdges.keys()).sort(); + const nodeAddressToSortedIndex = new Map(); sortedNodeAddresses.forEach((address, i) => { - nodeToSortedIndex.set(address, i); + nodeAddressToSortedIndex.set(address, i); }); - const sortedEdges = sortBy(Array.from(this.edges()), (x) => x.address); - const indexedEdges = sortedEdges.map(({src, dst, address}) => { - const srcIndex = NullUtil.get(nodeToSortedIndex.get(src)); - const dstIndex = NullUtil.get(nodeToSortedIndex.get(dst)); - return {srcIndex, dstIndex, address: EdgeAddress.toParts(address)}; + const sortedEdges = sortBy( + Array.from(this.edges({showDangling: true})), + (x) => x.address + ); + const indexedEdges: IndexedEdgeJSON[] = sortedEdges.map( + ({src, dst, address}) => { + const srcIndex = NullUtil.get(nodeAddressToSortedIndex.get(src)); + const dstIndex = NullUtil.get(nodeAddressToSortedIndex.get(dst)); + return {srcIndex, dstIndex, address: EdgeAddress.toParts(address)}; + } + ); + const sortedNodes = sortBy(Array.from(this.nodes()), (x) => x.address); + const indexedNodes: IndexedNodeJSON[] = sortedNodes.map(({address}) => { + const index = NullUtil.get(nodeAddressToSortedIndex.get(address)); + return {index}; }); const rawJSON = { - nodes: sortedNodeAddresses.map((x) => NodeAddress.toParts(x)), + sortedNodeAddresses: sortedNodeAddresses.map((x) => + NodeAddress.toParts(x) + ), edges: indexedEdges, + nodes: indexedNodes, }; const result = toCompat(COMPAT_INFO, rawJSON); this._maybeCheckInvariants(); @@ -689,21 +777,26 @@ export class Graph { * Deserializes a GraphJSON into a new Graph. */ static fromJSON(compatJson: GraphJSON): Graph { - const json = fromCompat(COMPAT_INFO, compatJson); - const nodesJSON: AddressJSON[] = json.nodes; - const edgesJSON: IndexedEdgeJSON[] = json.edges; + const { + nodes: nodesJSON, + edges: edgesJSON, + sortedNodeAddresses: sortedNodeAddressesJSON, + } = fromCompat(COMPAT_INFO, compatJson); + const sortedNodeAddresses = sortedNodeAddressesJSON.map( + NodeAddress.fromParts + ); const result = new Graph(); - const nodes: Node[] = nodesJSON.map((x) => ({ - address: NodeAddress.fromParts(x), - })); - nodes.forEach((n) => result.addNode(n)); + nodesJSON.forEach((j: IndexedNodeJSON) => { + const n: Node = {address: sortedNodeAddresses[j.index]}; + result.addNode(n); + }); edgesJSON.forEach(({address, srcIndex, dstIndex}) => { - const src = nodes[srcIndex]; - const dst = nodes[dstIndex]; + const src = sortedNodeAddresses[srcIndex]; + const dst = sortedNodeAddresses[dstIndex]; result.addEdge({ address: EdgeAddress.fromParts(address), - src: src.address, - dst: dst.address, + src: src, + dst: dst, }); }); return result; @@ -737,7 +830,7 @@ export class Graph { for (const node of graph.nodes()) { result.addNode(node); } - for (const edge of graph.edges()) { + for (const edge of graph.edges({showDangling: true})) { result.addEdge(edge); } } @@ -774,58 +867,74 @@ export class Graph { // values modulo deep equality (or, from context, an element of such a // class). - // Invariant 1. For a node address `a`, if `_nodes.has(a)` and - // `_nodes.get(a) === n`, then: - // 1. `n.address` equals `a`; - // 2. `_incidentEdges.has(n)`; + // Invariant 1. A node address `na` is 'referenced' if `_incidentEdges.has(na)`. + // 1.1 If a node is in the graph, then it is referenced by its address. + // 1.2 If a node has any incident edge, then it is referenced. + // 1.3 If a node is not in the graph and does not have incident edges, then + // it is not referenced. + const referencedNodesEncountered = new Set(); + // 1.1 for (const [address, node] of this._nodes) { if (node.address !== address) { throw new Error( - `bad node address: ${nodeToString(node)} does not match ${address}` + `bad node address for ${NodeAddress.toString(address)}` ); } - if (!this._incidentEdges.has(address)) { throw new Error( `missing incident-edges for ${NodeAddress.toString(address)}` ); } + referencedNodesEncountered.add(address); + } + // 1.2 + for (const edge of this._edges.values()) { + if (!this._incidentEdges.has(edge.src)) { + throw new Error( + `missing incident-edges for src of: ${edgeToString(edge)}` + ); + } + referencedNodesEncountered.add(edge.src); + if (!this._incidentEdges.has(edge.dst)) { + throw new Error( + `missing incident-edges for dst of: ${edgeToString(edge)}` + ); + } + referencedNodesEncountered.add(edge.dst); + } + // Check 1.3 by implication: for every address in + // referencedNodesEncountered, we've explicitly checked that it is present + // in _incidentEdges. + // + // Therefore, if the number of keys in _incidentEdges differs from the + // number of elements in referencedNodesEncountered, it must be because + // some elements in _incidentEdges were not present in + // referencedNodesEncountered, which means that they did not correspond to + // a node in the graph and did not have incident edges. + const numIncidentEntries = Array.from(this._incidentEdges.keys()).length; + if (numIncidentEntries !== referencedNodesEncountered.size) { + throw new Error("extra addresses in incident-edges"); } // Invariant 2. For an edge address `a`, if `_edges.has(a)` and // `_edges.get(a) === e`, then: // 1. `e.address` equals `a`; - // 2. `e.src` is in the graph; - // 3. `e.dst` is in the graph; + // 2. `e.src` is referenced; + // 3. `e.dst` is referenced; // 4. `_incidentEdges.get(e.dst).inEdges` contains `e`; and // 5. `_incidentEdges.get(e.src).outEdges` contains `e`. // - // We check 2.1, 2.2, and 2.3 here, and check 2.4 and 2.5 later for - // improved performance. + // 2.2 and 2.3 are implied by 2.4 and 2.5 respectively (as a node's address + // being available in _incidentEdges means that it is referenced). So we may + // ignore them. + // + // We check 2.1 here, and check 2.4 and 2.5 later for improved performance. for (const [address, edge] of this._edges.entries()) { if (edge.address !== address) { throw new Error( `bad edge address: ${edgeToString(edge)} does not match ${address}` ); } - if (!this._nodes.has(edge.src)) { - throw new Error(`missing src for edge: ${edgeToString(edge)}`); - } - if (!this._nodes.has(edge.dst)) { - throw new Error(`missing dst for edge: ${edgeToString(edge)}`); - } - } - - // Temporary Invariant - // Suppose that `_incidentEdges.has(n)`. Then `n` is in the graph. - // The {inEdges, outEdges} => incidentEdges refactor necessitated pulling - // this invariant out of invariants 3 and 4. However, the purpose of this - // refactor is actually to remove this invariant. So, there is no need to - // re-enumerate the invariants as this one will disappear shortly. - for (const addr of this._incidentEdges.keys()) { - if (!this._nodes.has(addr)) { - throw new Error(`spurious incident-edges`); - } } // Invariant 3. Suppose that `_incidentEdges.has(n)` and, let `es` be @@ -1005,6 +1114,6 @@ export function sortedEdgeAddressesFromJSON( export function sortedNodeAddressesFromJSON( json: GraphJSON ): $ReadOnlyArray { - const {nodes} = fromCompat(COMPAT_INFO, json); - return nodes.map((x) => NodeAddress.fromParts(x)); + const {sortedNodeAddresses} = fromCompat(COMPAT_INFO, json); + return sortedNodeAddresses.map((x) => NodeAddress.fromParts(x)); } diff --git a/src/core/graph.test.js b/src/core/graph.test.js index 77eab44..393c8f1 100644 --- a/src/core/graph.test.js +++ b/src/core/graph.test.js @@ -150,13 +150,28 @@ describe("core/graph", () => { g._nodes.set(dst.address, src); expect(() => g._checkInvariants()).toThrow("bad node address"); }); - // Invariant 1.2 - it("detects missing incident edges", () => { + it("detects missing incident edges corresponding to a node", () => { const g = new Graph().addNode(src); g._incidentEdges.delete(src.address); expect(() => g._checkInvariants()).toThrow("missing incident-edges"); }); + // Invariant 1.2 + it("detects missing incident edges corresponding to a dangling edge", () => { + const g = new Graph().addEdge(simpleEdge); + g._incidentEdges.delete(src.address); + expect(() => g._checkInvariants()).toThrow("missing incident-edges"); + }); + + // Invariant 1.3 + it("detects extra incident edges not corresponding to anything", () => { + const g = new Graph().addNode(src); + g._incidentEdges.set(dst.address, {inEdges: [], outEdges: []}); + expect(() => g._checkInvariants()).toThrow( + "extra addresses in incident-edges" + ); + }); + // Invariant 2.1 it("detects when an edge has bad address", () => { const g = simpleGraph(); @@ -167,20 +182,6 @@ describe("core/graph", () => { g._incidentEdges.get(src.address).outEdges = [differentAddressEdge]; expect(() => g._checkInvariants()).toThrow("bad edge address"); }); - // Invariant 2.2 - it("detects when an edge has missing src", () => { - const g = simpleGraph(); - g._nodes.delete(src.address); - g._incidentEdges.delete(src.address); - expect(() => g._checkInvariants()).toThrow("missing src"); - }); - // Invariant 2.3 - it("detects when an edge has missing dst", () => { - const g = simpleGraph(); - g._nodes.delete(dst.address); - g._incidentEdges.delete(dst.address); - expect(() => g._checkInvariants()).toThrow("missing dst"); - }); // Invariant 2.4 it("detects when an edge is missing in `_inEdges`", () => { const g = simpleGraph(); @@ -196,13 +197,6 @@ describe("core/graph", () => { expect(() => g._checkInvariants()).toThrow("missing out-edge"); }); - // Temporary invariant - it("detects spurious incident-edges", () => { - const g = new Graph(); - g._incidentEdges.set(src.address, {inEdges: [], outEdges: []}); - expect(() => g._checkInvariants()).toThrow("spurious incident-edges"); - }); - // Invariant 3.1 it("detects when an edge is duplicated in `_inEdges`", () => { const g = simpleGraph(); @@ -297,18 +291,6 @@ describe("core/graph", () => { expect(() => new Graph().addNode(n).toThrow("got EdgeAddress")); }); }); - describe("remove a node that is some edge's", () => { - it("src", () => { - expect(() => simpleGraph().removeNode(src.address)).toThrow( - "Attempted to remove" - ); - }); - it("dst", () => { - expect(() => simpleGraph().removeNode(dst.address)).toThrow( - "Attempted to remove" - ); - }); - }); it("distinct nodes with the same address", () => { const g = new Graph(); @@ -348,6 +330,29 @@ describe("core/graph", () => { expect(Array.from(graph.nodes())).toEqual([src]); expect(graph.node(src.address)).toEqual(src); }); + it("a graph with a node reference", () => { + // The dangling edge creates a node reference. + // This reference should not be discoverable via + // any of the node methods. + const graph = new Graph().addEdge(simpleEdge); + expect(graph.hasNode(src.address)).toBe(false); + expect(graph.hasNode(dst.address)).toBe(false); + expect(Array.from(graph.nodes())).toEqual([]); + expect(graph.node(src.address)).toEqual(undefined); + expect(graph.node(dst.address)).toEqual(undefined); + }); + it("a graph with a node node that downgrades to a reference", () => { + const graph = new Graph() + .addNode(src) + .addNode(dst) + .addEdge(simpleEdge) + .removeNode(src.address); + expect(graph.hasNode(src.address)).toBe(false); + expect(graph.hasNode(dst.address)).toBe(true); + expect(Array.from(graph.nodes())).toEqual([dst]); + expect(graph.node(src.address)).toEqual(undefined); + expect(graph.node(dst.address)).toEqual(dst); + }); it("a graph with the same node added twice", () => { const graph = new Graph().addNode(src).addNode(src); expect(graph.hasNode(src.address)).toBe(true); @@ -450,7 +455,6 @@ describe("core/graph", () => { }); describe("edge methods", () => { - const edgeArray = (g: Graph) => Array.from(g.edges()); describe("error on", () => { const p = Graph.prototype; const edgeAddrMethods = [p.removeEdge, p.hasEdge, p.edge]; @@ -470,19 +474,6 @@ describe("core/graph", () => { }); describe("addEdge edge validation", () => { - describe("throws on absent", () => { - it("src", () => { - expect(() => - new Graph().addNode(dst).addEdge(simpleEdge) - ).toThrow("Missing src"); - }); - it("dst", () => { - expect(() => - new Graph().addNode(src).addEdge(simpleEdge) - ).toThrow("Missing dst"); - }); - }); - it("throws on conflicting edge", () => { const e1 = edge("1", src, dst); const e2 = edge("1", src, src); @@ -544,13 +535,13 @@ describe("core/graph", () => { describe("concurrent modification in `edges`", () => { it("while in the middle of iteration", () => { const g = simpleGraph(); - const iterator = g.edges(); + const iterator = g.edges({showDangling: true}); g._modificationCount++; expect(() => iterator.next()).toThrow("Concurrent modification"); }); it("at exhaustion", () => { const g = new Graph(); - const iterator = g.edges(); + const iterator = g.edges({showDangling: true}); g._modificationCount++; expect(() => iterator.next()).toThrow("Concurrent modification"); }); @@ -566,13 +557,14 @@ describe("core/graph", () => { const e12 = partsEdge(["e", "1", "2"], src1, dst2); const e21 = partsEdge(["e", "2", "1"], src2, dst1); const e22 = partsEdge(["e", "2", "2"], src2, dst2); + const eDangling = partsEdge(["e", "2", "NaN"], src2, node("nope")); const graph = () => - [e11, e12, e21, e22].reduce( + [e11, e12, e21, e22, eDangling].reduce( (g, e) => g.addEdge(e), [src1, src2, dst1, dst2].reduce((g, n) => g.addNode(n), new Graph()) ); function expectEdges( - options: EdgesOptions | void, + options: EdgesOptions, expected: $ReadOnlyArray ) { const sort = (es) => sortBy(es, (e) => e.address); @@ -580,46 +572,28 @@ describe("core/graph", () => { sort(expected.slice()) ); } - it("finds all edges when no options are specified", () => { - expectEdges(undefined, [e11, e12, e21, e22]); + it("finds all edges when only showDangling:true is provided", () => { + expectEdges({showDangling: true}, [e11, e12, e21, e22, eDangling]); + }); + it("finds all non-dangling edges when only showDangling:false is provided", () => { + expectEdges({showDangling: false}, [e11, e12, e21, e22]); }); it("finds all edges when universal filters are specified", () => { expectEdges( { + showDangling: true, addressPrefix: EdgeAddress.fromParts(["e"]), srcPrefix: NodeAddress.fromParts(["src"]), - dstPrefix: NodeAddress.fromParts(["dst"]), + dstPrefix: NodeAddress.empty, }, - [e11, e12, e21, e22] + [e11, e12, e21, e22, eDangling] ); }); - it("requires `addressPrefix` to be present in provided options", () => { - expect(() => { - graph() - // $ExpectFlowError - .edges({srcPrefix: src1, dstPrefix: dst1}); - }).toThrow("Invalid address prefix: undefined"); - }); - it("requires `srcPrefix` to be present in provided options", () => { - expect(() => { - graph() - // $ExpectFlowError - .edges({addressPrefix: e11, dstPrefix: dst1}); - }).toThrow("Invalid src prefix: undefined"); - }); - it("requires `dstPrefix` to be present in provided options", () => { - expect(() => { - graph() - // $ExpectFlowError - .edges({addressPrefix: e11, srcPrefix: dst1}); - }).toThrow("Invalid dst prefix: undefined"); - }); it("finds edges by address prefix", () => { expectEdges( { + showDangling: true, addressPrefix: EdgeAddress.fromParts(["e", "1"]), - srcPrefix: NodeAddress.empty, - dstPrefix: NodeAddress.empty, }, [e11, e12] ); @@ -627,9 +601,8 @@ describe("core/graph", () => { it("finds edges by src prefix", () => { expectEdges( { - addressPrefix: EdgeAddress.empty, + showDangling: true, srcPrefix: NodeAddress.fromParts(["src", "1"]), - dstPrefix: NodeAddress.empty, }, [e11, e12] ); @@ -637,8 +610,7 @@ describe("core/graph", () => { it("finds edges by dst prefix", () => { expectEdges( { - addressPrefix: EdgeAddress.empty, - srcPrefix: NodeAddress.empty, + showDangling: true, dstPrefix: NodeAddress.fromParts(["dst", "1"]), }, [e11, e21] @@ -647,6 +619,7 @@ describe("core/graph", () => { it("yields nothing for disjoint filters", () => { expectEdges( { + showDangling: true, addressPrefix: EdgeAddress.fromParts(["e", "1"]), srcPrefix: NodeAddress.fromParts(["src", "2"]), dstPrefix: NodeAddress.empty, @@ -654,14 +627,32 @@ describe("core/graph", () => { [] ); }); - it("yields appropriate filter intersection", () => { + it("yields appropriate filter intersection for srcPrefix and dstPrefix", () => { expectEdges( { - addressPrefix: EdgeAddress.empty, - srcPrefix: NodeAddress.fromParts(["src", "1"]), + showDangling: true, + srcPrefix: NodeAddress.fromParts(["src", "2"]), dstPrefix: NodeAddress.fromParts(["dst", "2"]), }, - [e12] + [e22] + ); + }); + it("yields appropriate filter intersection with showDangling: false", () => { + expectEdges( + { + showDangling: false, + srcPrefix: NodeAddress.fromParts(["src", "2"]), + }, + [e21, e22] + ); + }); + it("yields appropriate filter intersection with showDangling: true", () => { + expectEdges( + { + showDangling: true, + srcPrefix: NodeAddress.fromParts(["src", "2"]), + }, + [e21, e22, eDangling] ); }); }); @@ -675,7 +666,13 @@ describe("core/graph", () => { expect(new Graph().edge(simpleEdge.address)).toBe(undefined); }); it("`edges` is empty", () => { - expect(edgeArray(new Graph())).toHaveLength(0); + const edges = Array.from(new Graph().edges({showDangling: true})); + expect(edges).toHaveLength(0); + }); + it("`isDanglingEdge` is undefined", () => { + expect(new Graph().isDanglingEdge(simpleEdge.address)).toBe( + undefined + ); }); }); @@ -687,9 +684,15 @@ describe("core/graph", () => { expect(simpleGraph().edge(simpleEdge.address)).toEqual(simpleEdge); }); it("`edges` contains the edge", () => { - const edgeArray = (g: Graph) => Array.from(g.edges()); + const edgeArray = (g: Graph) => + Array.from(g.edges({showDangling: true})); expect(edgeArray(simpleGraph())).toEqual([simpleEdge]); }); + it("`isDanglingEdge` is false", () => { + expect(simpleGraph().isDanglingEdge(simpleEdge.address)).toBe( + false + ); + }); }); describe("with edge added and removed", () => { @@ -701,8 +704,16 @@ describe("core/graph", () => { it("`edge` returns undefined", () => { expect(removedGraph().edge(simpleEdge.address)).toBe(undefined); }); + it("`isDanglingEdge` returns undefined", () => { + expect(removedGraph().isDanglingEdge(simpleEdge.address)).toBe( + undefined + ); + }); it("`edges` is empty", () => { - expect(edgeArray(removedGraph())).toHaveLength(0); + const edges = Array.from( + removedGraph().edges({showDangling: true}) + ); + expect(edges).toHaveLength(0); }); it("nodes were not removed", () => { expect(removedGraph().hasNode(src.address)).toBe(true); @@ -731,7 +742,50 @@ describe("core/graph", () => { expect(quiver().edge(e2.address)).toEqual(e2); }); it("both edges are retrievable from `edges`", () => { - expect(edgeArray(quiver()).sort()).toEqual([e1, e2].sort()); + const edges = Array.from(quiver().edges({showDangling: true})); + expect(edges).toEqual([e1, e2]); + }); + }); + + describe("with dangling edges", () => { + it("in the case where an edge was always dangling", () => { + const g = new Graph().addEdge(simpleEdge); + expect(g.hasEdge(simpleEdge.address)).toBe(true); + expect(Array.from(g.edges({showDangling: true}))).toEqual([ + simpleEdge, + ]); + expect(Array.from(g.edges({showDangling: false}))).toEqual([]); + expect(g.edge(simpleEdge.address)).toEqual(simpleEdge); + expect(g.isDanglingEdge(simpleEdge.address)).toBe(true); + }); + it("in the case where an edge became dangling after being added", () => { + const g = new Graph() + .addNode(src) + .addNode(dst) + .addEdge(simpleEdge) + .removeNode(src.address); + expect(g.hasEdge(simpleEdge.address)).toBe(true); + expect(Array.from(g.edges({showDangling: true}))).toEqual([ + simpleEdge, + ]); + expect(Array.from(g.edges({showDangling: false}))).toEqual([]); + expect(g.edge(simpleEdge.address)).toEqual(simpleEdge); + expect(g.isDanglingEdge(simpleEdge.address)).toBe(true); + }); + it("in the case where an edge ceased being dangling after being added", () => { + const g = new Graph() + .addEdge(simpleEdge) + .addNode(src) + .addNode(dst); + expect(g.hasEdge(simpleEdge.address)).toBe(true); + expect(Array.from(g.edges({showDangling: true}))).toEqual([ + simpleEdge, + ]); + expect(Array.from(g.edges({showDangling: false}))).toEqual([ + simpleEdge, + ]); + expect(g.edge(simpleEdge.address)).toEqual(simpleEdge); + expect(g.isDanglingEdge(simpleEdge.address)).toBe(false); }); }); }); @@ -739,7 +793,9 @@ describe("core/graph", () => { describe("idempotency of", () => { it("`addEdge`", () => { const g = simpleGraph().addEdge(simpleEdge); - expect(edgeArray(g)).toEqual([simpleEdge]); + expect(Array.from(g.edges({showDangling: true}))).toEqual([ + simpleEdge, + ]); expect( Array.from( g.neighbors(src.address, { @@ -754,7 +810,7 @@ describe("core/graph", () => { const g = simpleGraph() .removeEdge(simpleEdge.address) .removeEdge(simpleEdge.address); - expect(edgeArray(g)).toHaveLength(0); + expect(Array.from(g.edges({showDangling: true}))).toHaveLength(0); }); }); }); @@ -791,20 +847,32 @@ describe("core/graph", () => { const foo = partsNode(["foo", "suffix"]); const loop = partsNode(["loop"]); const isolated = partsNode(["isolated"]); + // halfIsolated is the src of one edge, but that edge + // is a dangling edge (its dst is not in the graph). + // It's included so we can verify it has no neighbors. + const halfIsolated = partsNode(["halfIsolated"]); const foo_loop = partsEdge(["foo", "1"], foo, loop); const loop_foo = partsEdge(["foo", "2"], loop, foo); const loop_loop = partsEdge(["loop"], loop, loop); const repeated_loop_foo = partsEdge(["repeated", "foo"], loop, foo); + const dangling = partsEdge( + ["dangling"], + halfIsolated, + node("nonexistent") + ); + function quiver() { return new Graph() .addNode(foo) .addNode(loop) .addNode(isolated) + .addNode(halfIsolated) .addEdge(foo_loop) .addEdge(loop_foo) .addEdge(loop_loop) - .addEdge(repeated_loop_foo); + .addEdge(repeated_loop_foo) + .addEdge(dangling); } function expectNeighbors( @@ -844,6 +912,19 @@ describe("core/graph", () => { ); }); + it("half-isolated node has no neighbors", () => { + // Verifies that dangling edges are not included in neighbors. + expectNeighbors( + halfIsolated.address, + { + direction: Direction.ANY, + nodePrefix: NodeAddress.empty, + edgePrefix: EdgeAddress.empty, + }, + [] + ); + }); + function expectLoopNeighbors(dir, nodeParts, edgeParts, expected) { const options = { direction: dir, @@ -1200,6 +1281,16 @@ describe("core/graph", () => { expect(graph1().equals(graph3)).toBe(true); expect(graph2().equals(graph3)).toBe(true); }); + it("merges a dangling edge with its src and dst", () => { + const g1 = new Graph().addEdge(simpleEdge); + const g2 = new Graph().addNode(src).addNode(dst); + const expected = new Graph() + .addNode(src) + .addNode(dst) + .addEdge(simpleEdge); + const merged = Graph.merge([g1, g2]); + expect(merged.equals(expected)).toBe(true); + }); it("rejects graphs with conflicting edges", () => { const g1 = new Graph() .addNode(foo) @@ -1223,6 +1314,10 @@ describe("core/graph", () => { .addEdge(differentAddressEdge); expect(graph.toJSON()).toMatchSnapshot(); }); + it("a graph with a dangling edge", () => { + const graph = new Graph().addEdge(simpleEdge); + expect(graph.toJSON()).toMatchSnapshot(); + }); it("an advanced graph", () => { const graph = advancedGraph().graph1(); expect(graph.toJSON()).toMatchSnapshot(); @@ -1244,6 +1339,10 @@ describe("core/graph", () => { const g = new Graph().addNode(src).addNode(dst); expectCompose(g); }); + it("for a graph with a dangling edge", () => { + const g = new Graph().addEdge(simpleEdge); + expectCompose(g); + }); it("for a graph with nodes added and removed", () => { const g = new Graph() .addNode(src) @@ -1251,6 +1350,12 @@ describe("core/graph", () => { .removeNode(src.address); expectCompose(g); }); + it("a graph with a dangling edge added and removed", () => { + const g = new Graph() + .addEdge(simpleEdge) + .removeEdge(simpleEdge.address); + expectCompose(g); + }); it("for a graph with nodes and edges", () => { const g = new Graph() .addNode(src) diff --git a/src/core/graphTestUtil.js b/src/core/graphTestUtil.js index 09f6bba..4a35e29 100644 --- a/src/core/graphTestUtil.js +++ b/src/core/graphTestUtil.js @@ -71,6 +71,12 @@ export function advancedGraph() { const hom2 = partsEdge(["hom", "2"], src, dst); const loop = node("loop"); const loop_loop = edge("loop", loop, loop); + + const halfIsolated = node("halfIsolated"); + const phantomNode = node("phantom"); + const halfDanglingEdge = edge("half-dangling", halfIsolated, phantomNode); + const fullDanglingEdge = edge("full-dangling", phantomNode, phantomNode); + const isolated = node("isolated"); const graph1 = () => new Graph() @@ -80,13 +86,15 @@ export function advancedGraph() { .addNode(isolated) .addEdge(hom1) .addEdge(hom2) - .addEdge(loop_loop); + .addEdge(loop_loop) + .addNode(halfIsolated) + .addEdge(halfDanglingEdge) + .addEdge(fullDanglingEdge); // graph2 is logically equivalent to graph1, but is constructed with very // different history. // Use this to check that logically equivalent graphs are treated // equivalently, regardless of their history. - const phantomNode = node("phantom"); const phantomEdge1 = edge("phantom", src, phantomNode); const phantomEdge2 = edge("not-so-isolated", src, isolated); @@ -103,33 +111,63 @@ export function advancedGraph() { // N: [phantomNode, src], E: [phantomEdge1] .addNode(isolated) // N: [phantomNode, src, isolated], E: [phantomEdge1] + .addNode(halfIsolated) + // N: [phantomNode, src, isolated, halfIsolated] + // E: [phantomEdge1] + .addEdge(halfDanglingEdge) + // N: [phantomNode, src, isolated, halfIsolated] + // E: [phantomEdge1, halfDanglingEdge] + .addEdge(fullDanglingEdge) + // N: [phantomNode, src, isolated, halfIsolated] + // E: [phantomEdge1, halfDanglingEdge, fullDanglingEdge] .removeEdge(phantomEdge1.address) - // N: [phantomNode, src, isolated], E: [] + // N: [phantomNode, src, isolated, halfIsolated] + // E: [halfDanglingEdge, fullDanglingEdge] .addNode(dst) - // N: [phantomNode, src, isolated, dst], E: [] + // N: [phantomNode, src, isolated, halfIsolated, dst] + // E: [halfDanglingEdge, fullDanglingEdge] .addEdge(hom1) - // N: [phantomNode, src, isolated, dst], E: [hom1] + // N: [phantomNode, src, isolated, halfIsolated, dst] + // E: [halfDanglingEdge, fullDanglingEdge, hom1] .addEdge(phantomEdge2) - // N: [phantomNode, src, isolated, dst], E: [hom1, phantomEdge2] + // N: [phantomNode, src, isolated, halfIsolated, dst] + // E: [halfDanglingEdge, fullDanglingEdge, hom1, phantomEdge2] .addEdge(hom2) - // N: [phantomNode, src, isolated, dst], E: [hom1, phantomEdge2, hom2] + // N: [phantomNode, src, isolated, halfIsolated, dst] + // E: [halfDanglingEdge, fullDanglingEdge, hom1, phantomEdge2, hom2] .removeEdge(hom1.address) - // N: [phantomNode, src, isolated, dst], E: [phantomEdge2, hom2] + // N: [phantomNode, src, isolated, halfIsolated, dst] + // E: [halfDanglingEdge, fullDanglingEdge, phantomEdge2, hom2] .removeNode(phantomNode.address) - // N: [src, isolated, dst], E: [phantomEdge2, hom2] + // N: [src, isolated, halfIsolated, dst] + // E: [halfDanglingEdge, fullDanglingEdge, phantomEdge2, hom2] .removeEdge(phantomEdge2.address) - // N: [src, isolated, dst], E: [hom2] + // N: [src, isolated, halfIsolated, dst] + // E: [halfDanglingEdge, fullDanglingEdge, hom2] .removeNode(isolated.address) - // N: [src, dst], E: [hom2] + // N: [src, halfIsolated, dst] + // E: [halfDanglingEdge, fullDanglingEdge, hom2] .addNode(isolated) - // N: [src, dst, isolated], E: [hom2] + // N: [src, halfIsolated, dst, isolated] + // E: [halfDanglingEdge, fullDanglingEdge, hom2] .addNode(loop) - // N: [src, dst, isolated, loop], E: [hom2] + // N: [src, halfIsolated, dst, isolated, loop] + // E: [halfDanglingEdge, fullDanglingEdge, hom2] .addEdge(loop_loop) - // N: [src, dst, isolated, loop], E: [hom2, loop_loop] + // N: [src, halfIsolated, dst, isolated, loop] + // E: [halfDanglingEdge, fullDanglingEdge, hom2, loop_loop] .addEdge(hom1); - // N: [src, dst, isolated, loop], E: [hom2, loop_loop, hom1] - const nodes = {src, dst, loop, isolated, phantomNode}; - const edges = {hom1, hom2, loop_loop, phantomEdge1, phantomEdge2}; + // N: [src, halfIsolated, dst, isolated, loop] + // E: [halfDanglingEdge, fullDanglingEdge, hom2, loop_loop, hom1] + const nodes = {src, dst, loop, isolated, phantomNode, halfIsolated}; + const edges = { + hom1, + hom2, + loop_loop, + phantomEdge1, + phantomEdge2, + halfDanglingEdge, + fullDanglingEdge, + }; return {nodes, edges, graph1, graph2}; } diff --git a/src/core/pagerankGraph.js b/src/core/pagerankGraph.js index 07e8f4c..0a63122 100644 --- a/src/core/pagerankGraph.js +++ b/src/core/pagerankGraph.js @@ -7,7 +7,6 @@ import { Graph, type Node, type Edge, - type EdgesOptions, type NodeAddressT, type EdgeAddressT, type GraphJSON, @@ -47,6 +46,12 @@ export type WeightedEdge = {| +weight: EdgeWeight, |}; +export type PagerankGraphEdgesOptions = {| + +addressPrefix?: EdgeAddressT, + +srcPrefix?: NodeAddressT, + +dstPrefix?: NodeAddressT, +|}; + export type ScoredNeighbor = {| // The neighbor node, with its score +scoredNode: ScoredNode, @@ -236,7 +241,7 @@ export class PagerankGraph { const newWeight = previousWeight + weight; this._totalOutWeight.set(node, newWeight); }; - for (const edge of this._graph.edges()) { + for (const edge of this._graph.edges({showDangling: false})) { const weights = edgeEvaluator(edge); this._edgeWeights.set(edge.address, weights); addOutWeight(edge.src, weights.toWeight); @@ -310,10 +315,24 @@ export class PagerankGraph { * Optionally, provide an EdgesOptions parameter to return an * iterator containing edges matching the EdgesOptions prefix * filter parameters. See Graph.edges for details. + * + * In contrast to Graph.edges, dangling edges will never be included, + * as we do not assign weights to danging edges. */ - edges(options?: EdgesOptions): Iterator { + edges(options?: PagerankGraphEdgesOptions): Iterator { this._verifyGraphNotModified(); - const iterator = this._graph.edges(options); + const graphOptions = { + showDangling: false, + addressPrefix: undefined, + srcPrefix: undefined, + dstPrefix: undefined, + }; + if (options != null) { + graphOptions.addressPrefix = options.addressPrefix; + graphOptions.srcPrefix = options.srcPrefix; + graphOptions.dstPrefix = options.dstPrefix; + } + const iterator = this._graph.edges(graphOptions); return this._edgesIterator(iterator); } @@ -332,7 +351,7 @@ export class PagerankGraph { edge(a: EdgeAddressT): ?WeightedEdge { this._verifyGraphNotModified(); const edge = this._graph.edge(a); - if (edge != null) { + if (edge != null && this._graph.isDanglingEdge(a) === false) { const weight = NullUtil.get(this._edgeWeights.get(edge.address)); return {edge, weight}; } @@ -548,12 +567,16 @@ export class PagerankGraph { this._verifyGraphNotModified(); const graphJSON = this.graph().toJSON(); - const nodes = sortedNodeAddressesFromJSON(graphJSON); + const nodes = sortedNodeAddressesFromJSON(graphJSON).filter((x) => + this.graph().hasNode(x) + ); const scores: number[] = nodes.map((x) => NullUtil.get(this._scores.get(x)) ); - const edgeAddresses = sortedEdgeAddressesFromJSON(graphJSON); + const edgeAddresses = sortedEdgeAddressesFromJSON(graphJSON).filter( + (a) => this.graph().isDanglingEdge(a) === false + ); const edgeWeights: EdgeWeight[] = edgeAddresses.map((x) => NullUtil.get(this._edgeWeights.get(x)) ); @@ -581,18 +604,22 @@ export class PagerankGraph { } = fromCompat(COMPAT_INFO, json); const graph = Graph.fromJSON(graphJSON); - const nodes = sortedNodeAddressesFromJSON(graphJSON); + const nodeAddresses = sortedNodeAddressesFromJSON(graphJSON).filter((x) => + graph.hasNode(x) + ); const scoreMap: Map = new Map(); - for (let i = 0; i < nodes.length; i++) { - scoreMap.set(nodes[i], scores[i]); + for (let i = 0; i < nodeAddresses.length; i++) { + scoreMap.set(nodeAddresses[i], scores[i]); } - const edges = sortedEdgeAddressesFromJSON(graphJSON); + const edgeAddresses = sortedEdgeAddressesFromJSON(graphJSON).filter( + (x) => graph.isDanglingEdge(x) === false + ); const edgeWeights: Map = new Map(); - for (let i = 0; i < edges.length; i++) { + for (let i = 0; i < edgeAddresses.length; i++) { const toWeight = toWeights[i]; const froWeight = froWeights[i]; - edgeWeights.set(edges[i], {toWeight, froWeight}); + edgeWeights.set(edgeAddresses[i], {toWeight, froWeight}); } function evaluator(e: Edge): EdgeWeight { diff --git a/src/core/pagerankGraph.test.js b/src/core/pagerankGraph.test.js index 07fa51a..ea59df3 100644 --- a/src/core/pagerankGraph.test.js +++ b/src/core/pagerankGraph.test.js @@ -16,6 +16,7 @@ import { DEFAULT_CONVERGENCE_THRESHOLD, DEFAULT_ALPHA, DEFAULT_SEED, + type PagerankGraphEdgesOptions, } from "./pagerankGraph"; import {advancedGraph, node, partsNode, partsEdge} from "./graphTestUtil"; import * as NullUtil from "../util/null"; @@ -162,10 +163,10 @@ describe("core/pagerankGraph", () => { }); describe("edge/edges", () => { - it("edges returns the same edges as are in the graph", () => { + it("edges returns the non-dangling edges in the base graph", () => { const g = advancedGraph().graph1(); const pg = new PagerankGraph(g, defaultEvaluator); - const graphEdges = Array.from(g.edges()); + const graphEdges = Array.from(g.edges({showDangling: false})); const pgEdges = Array.from(pg.edges()).map((x) => x.edge); expect(graphEdges.length).toEqual(pgEdges.length); const addressAccessor = (x: Edge) => x.address; @@ -194,6 +195,12 @@ describe("core/pagerankGraph", () => { expect(pg.edge(EdgeAddress.empty)).toBe(undefined); }); + it("edge returns null for dangling edge", () => { + const {graph1, edges} = advancedGraph(); + const pg = new PagerankGraph(graph1(), defaultEvaluator); + expect(pg.edge(edges.halfDanglingEdge.address)).toEqual(undefined); + }); + it("edge and edges both throw an error if underlying graph is modified", () => { const pg = new PagerankGraph(nonEmptyGraph(), defaultEvaluator); pg.graph().addNode(node("foo")); @@ -286,13 +293,17 @@ describe("core/pagerankGraph", () => { }; const pagerankGraph = () => new PagerankGraph(graph(), defaultEvaluator); - function expectConsistentEdges(options: EdgesOptions | void) { + function expectConsistentEdges(options: PagerankGraphEdgesOptions | void) { const pagerankGraphEdges = Array.from(pagerankGraph().edges(options)); pagerankGraphEdges.forEach((e) => { expect(e.weight.froWeight).toBe(0); expect(e.weight.toWeight).toBe(1); }); - const graphEdges = Array.from(graph().edges(options)); + const graphOptions: EdgesOptions = + options == null + ? {showDangling: false} + : {...options, showDangling: false}; + const graphEdges = Array.from(graph().edges(graphOptions)); expect(pagerankGraphEdges.map((e) => e.edge)).toEqual(graphEdges); } @@ -310,21 +321,15 @@ describe("core/pagerankGraph", () => { it("finds edges by address prefix", () => { expectConsistentEdges({ addressPrefix: EdgeAddress.fromParts(["e", "1"]), - srcPrefix: NodeAddress.empty, - dstPrefix: NodeAddress.empty, }); }); it("finds edges by src prefix", () => { expectConsistentEdges({ - addressPrefix: EdgeAddress.empty, srcPrefix: NodeAddress.fromParts(["src", "1"]), - dstPrefix: NodeAddress.empty, }); }); it("finds edges by dst prefix", () => { expectConsistentEdges({ - addressPrefix: EdgeAddress.empty, - srcPrefix: NodeAddress.empty, dstPrefix: NodeAddress.fromParts(["dst", "1"]), }); }); @@ -332,42 +337,15 @@ describe("core/pagerankGraph", () => { expectConsistentEdges({ addressPrefix: EdgeAddress.fromParts(["e", "1"]), srcPrefix: NodeAddress.fromParts(["src", "2"]), - dstPrefix: NodeAddress.empty, }); }); it("yields appropriate filter intersection", () => { expectConsistentEdges({ - addressPrefix: EdgeAddress.empty, srcPrefix: NodeAddress.fromParts(["src", "1"]), dstPrefix: NodeAddress.fromParts(["dst", "2"]), }); }); }); - - describe("edge filter options", () => { - it("requires `addressPrefix` to be present in provided options", () => { - expect(() => { - pagerankGraph() - // $ExpectFlowError - .edges({srcPrefix: src1, dstPrefix: dst1}); - }).toThrow("Invalid address prefix: undefined"); - }); - it("requires `srcPrefix` to be present in provided options", () => { - expect(() => { - pagerankGraph() - // $ExpectFlowError - .edges({addressPrefix: e11, dstPrefix: dst1}); - }).toThrow("Invalid src prefix: undefined"); - }); - - it("requires `dstPrefix` to be present in provided options", () => { - expect(() => { - pagerankGraph() - // $ExpectFlowError - .edges({addressPrefix: e11, srcPrefix: dst1}); - }).toThrow("Invalid dst prefix: undefined"); - }); - }); }); describe("neighbors", () => { diff --git a/src/plugins/git/__snapshots__/createGraph.test.js.snap b/src/plugins/git/__snapshots__/createGraph.test.js.snap index c99d680..134acfe 100644 --- a/src/plugins/git/__snapshots__/createGraph.test.js.snap +++ b/src/plugins/git/__snapshots__/createGraph.test.js.snap @@ -4,7 +4,7 @@ exports[`plugins/git/createGraph createGraph processes a simple repository 1`] = Array [ Object { "type": "sourcecred/graph", - "version": "0.4.0", + "version": "0.5.0", }, Object { "edges": Array [ @@ -115,6 +115,32 @@ Array [ }, ], "nodes": Array [ + Object { + "index": 0, + }, + Object { + "index": 1, + }, + Object { + "index": 2, + }, + Object { + "index": 3, + }, + Object { + "index": 4, + }, + Object { + "index": 5, + }, + Object { + "index": 6, + }, + Object { + "index": 7, + }, + ], + "sortedNodeAddresses": Array [ Array [ "sourcecred", "git", diff --git a/src/plugins/git/createGraph.test.js b/src/plugins/git/createGraph.test.js index 39e389a..76b34be 100644 --- a/src/plugins/git/createGraph.test.js +++ b/src/plugins/git/createGraph.test.js @@ -22,7 +22,7 @@ describe("plugins/git/createGraph", () => { throw new Error("Found non-commit node"); } } - for (const {address} of graph.edges()) { + for (const {address} of graph.edges({showDangling: true})) { if (!EdgeAddress.hasPrefix(address, EdgePrefix.hasParent)) { throw new Error("Found non-has-parent edge"); } diff --git a/src/plugins/github/__snapshots__/createGraph.test.js.snap b/src/plugins/github/__snapshots__/createGraph.test.js.snap index 3fd4c41..ff22e52 100644 --- a/src/plugins/github/__snapshots__/createGraph.test.js.snap +++ b/src/plugins/github/__snapshots__/createGraph.test.js.snap @@ -4,7 +4,7 @@ exports[`plugins/github/createGraph example graph matches snapshot 1`] = ` Array [ Object { "type": "sourcecred/graph", - "version": "0.4.0", + "version": "0.5.0", }, Object { "edges": Array [ @@ -2560,6 +2560,158 @@ Array [ }, ], "nodes": Array [ + Object { + "index": 0, + }, + Object { + "index": 1, + }, + Object { + "index": 2, + }, + Object { + "index": 3, + }, + Object { + "index": 4, + }, + Object { + "index": 5, + }, + Object { + "index": 6, + }, + Object { + "index": 7, + }, + Object { + "index": 8, + }, + Object { + "index": 9, + }, + Object { + "index": 10, + }, + Object { + "index": 11, + }, + Object { + "index": 12, + }, + Object { + "index": 13, + }, + Object { + "index": 14, + }, + Object { + "index": 15, + }, + Object { + "index": 16, + }, + Object { + "index": 17, + }, + Object { + "index": 18, + }, + Object { + "index": 19, + }, + Object { + "index": 20, + }, + Object { + "index": 21, + }, + Object { + "index": 22, + }, + Object { + "index": 23, + }, + Object { + "index": 24, + }, + Object { + "index": 25, + }, + Object { + "index": 26, + }, + Object { + "index": 27, + }, + Object { + "index": 28, + }, + Object { + "index": 29, + }, + Object { + "index": 30, + }, + Object { + "index": 31, + }, + Object { + "index": 32, + }, + Object { + "index": 33, + }, + Object { + "index": 34, + }, + Object { + "index": 35, + }, + Object { + "index": 36, + }, + Object { + "index": 37, + }, + Object { + "index": 38, + }, + Object { + "index": 39, + }, + Object { + "index": 40, + }, + Object { + "index": 41, + }, + Object { + "index": 42, + }, + Object { + "index": 43, + }, + Object { + "index": 44, + }, + Object { + "index": 45, + }, + Object { + "index": 46, + }, + Object { + "index": 47, + }, + Object { + "index": 48, + }, + Object { + "index": 49, + }, + ], + "sortedNodeAddresses": Array [ Array [ "sourcecred", "git",