Implement bare-bones sc2 merge and score (#1815)

This adds the missing `merge` and `score` commands to the sc2 CLI.
`merge` currently just merges the plugin graphs, and doesn't yet support
identity resolution or weight overrides.

`score` computes the new data output format (cf #1773) and writes it to
disk. It doesn't yet support using custom parameters.

Test plan:
Follow the test plans for the previous commits, then run `sc2 merge`.
It will create a combined graph at `output/graph.json`, which will
contain data for both Discourse and GitHub. Then run `sc2 score` and
the `output/cred.json` file will contain scores for the combined
graph.
This commit is contained in:
Dandelion Mané 2020-05-30 15:26:01 -07:00 committed by GitHub
parent c2510d0c1d
commit 40426f353c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 115 additions and 0 deletions

53
src/cli2/merge.js Normal file
View File

@ -0,0 +1,53 @@
// @flow
import fs from "fs-extra";
import stringify from "json-stable-stringify";
import {join as pathJoin} from "path";
import {LoggingTaskReporter} from "../util/taskReporter";
import type {Command} from "./command";
import {makePluginDir, loadInstanceConfig} from "./common";
import {
toJSON as weightedGraphToJSON,
fromJSON as weightedGraphFromJSON,
type WeightedGraph,
merge,
} from "../core/weightedGraph";
function die(std, message) {
std.err("fatal: " + message);
return 1;
}
const mergeCommand: Command = async (args, std) => {
if (args.length !== 0) {
return die(std, "usage: sourcecred merge");
}
const taskReporter = new LoggingTaskReporter();
taskReporter.start("merge");
const baseDir = process.cwd();
const config = await loadInstanceConfig(baseDir);
const graphOutputPrefix = ["output", "graphs"];
async function loadGraph(pluginName): Promise<WeightedGraph> {
const outputDir = makePluginDir(baseDir, graphOutputPrefix, pluginName);
const outputPath = pathJoin(outputDir, "graph.json");
const graphJSON = JSON.parse(await fs.readFile(outputPath));
return weightedGraphFromJSON(graphJSON);
}
const pluginNames = Array.from(config.bundledPlugins.keys());
const graphs = await Promise.all(pluginNames.map(loadGraph));
// TODO: Support identity merging.
// TODO: Support weight overrides.
const combinedGraph = merge(graphs);
const outputPath = pathJoin(baseDir, "output", "graph.json");
const serializedGraph = stringify(weightedGraphToJSON(combinedGraph));
await fs.writeFile(outputPath, serializedGraph);
taskReporter.finish("merge");
return 0;
};
export default mergeCommand;

56
src/cli2/score.js Normal file
View File

@ -0,0 +1,56 @@
// @flow
import fs from "fs-extra";
import stringify from "json-stable-stringify";
import {join as pathJoin} from "path";
import type {Command} from "./command";
import {loadInstanceConfig} from "./common";
import {fromJSON as weightedGraphFromJSON} from "../core/weightedGraph";
import {defaultParams} from "../analysis/timeline/params";
import {TimelineCred} from "../analysis/timeline/timelineCred";
import {LoggingTaskReporter} from "../util/taskReporter";
import {
fromTimelineCredAndPlugins,
COMPAT_INFO as OUTPUT_COMPAT_INFO,
} from "../analysis/output";
import {toCompat} from "../util/compat";
function die(std, message) {
std.err("fatal: " + message);
return 1;
}
const scoreCommand: Command = async (args, std) => {
if (args.length !== 0) {
return die(std, "usage: sourcecred score");
}
const taskReporter = new LoggingTaskReporter();
taskReporter.start("score");
const baseDir = process.cwd();
const config = await loadInstanceConfig(baseDir);
const graphFilePath = pathJoin(baseDir, "output", "graph.json");
const graphJSON = JSON.parse(await fs.readFile(graphFilePath));
const graph = weightedGraphFromJSON(graphJSON);
const plugins = Array.from(config.bundledPlugins.values());
const declarations = plugins.map((x) => x.declaration());
// TODO: Support loading params from config.
const params = defaultParams();
const tc = await TimelineCred.compute({
weightedGraph: graph,
params,
plugins: declarations,
});
const output = fromTimelineCredAndPlugins(tc, declarations);
const outputJSON = stringify(toCompat(OUTPUT_COMPAT_INFO, output));
const outputPath = pathJoin(baseDir, "output", "cred.json");
await fs.writeFile(outputPath, outputJSON);
taskReporter.finish("score");
return 0;
};
export default scoreCommand;

View File

@ -4,6 +4,8 @@ import type {Command} from "./command";
import load from "./load"; import load from "./load";
import graph from "./graph"; import graph from "./graph";
import merge from "./merge";
import score from "./score";
const sourcecred: Command = async (args, std) => { const sourcecred: Command = async (args, std) => {
if (args.length === 0) { if (args.length === 0) {
@ -15,6 +17,10 @@ const sourcecred: Command = async (args, std) => {
return load(args.slice(1), std); return load(args.slice(1), std);
case "graph": case "graph":
return graph(args.slice(1), std); return graph(args.slice(1), std);
case "merge":
return merge(args.slice(1), std);
case "score":
return score(args.slice(1), std);
default: default:
std.err("fatal: unknown command: " + JSON.stringify(args[0])); std.err("fatal: unknown command: " + JSON.stringify(args[0]));
return 1; return 1;