Replace TimelineCredConfig with array of plugins (#1367)

The PluginDeclaration has all of the information we need to configure
TimelineCred: it knows all the node and edge types, as well as which
node types are user (or scoring) node types.

Therefore, we can replace the ad-hoc config object with a simple array
of plugin declarations. Since the plugins will be saved as part of the
TimelineCred, it means the UI can configure to only show information for
plugins that are actually in scope.

Test plan: `yarn test` passes, and the prototype still works. Snapshots
updated.
This commit is contained in:
Dandelion Mané 2019-09-10 19:36:12 +02:00 committed by GitHub
parent dcf4010ff0
commit 65f22a0a74
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 75 additions and 101 deletions

View File

@ -8,6 +8,7 @@ import {toCompat, fromCompat, type Compatible} from "../../util/compat";
import {type Interval} from "./interval";
import {timelinePagerank} from "./timelinePagerank";
import {distributionToCred} from "./distributionToCred";
import {type PluginDeclaration, combineTypes} from "../pluginDeclaration";
import {
Graph,
type GraphJSON,
@ -21,7 +22,6 @@ import {
toJSON as weightsToJSON,
fromJSON as weightsFromJSON,
} from "../weights";
import {type NodeAndEdgeTypes} from "../types";
export type {Interval} from "./interval";
@ -61,26 +61,6 @@ export type TimelineCredParameters = {|
+weights: Weights,
|};
/**
* Configuration for computing TimelineCred
*
* Unlike the parameters, the config is expected to be static.
* It's code-level config that isolates the TimelineCred algorithms from
* specific plugin-level details about which nodes addresses are used for scoring,
* etc.
*
* A default config is available in `src/plugins/defaultCredConfig`
*/
export type TimelineCredConfig = {|
// Cred is normalized so that for a given interval, the total score of all
// nodes matching this prefix will be equal to the total weight of nodes in
// the interval.
+scoreNodePrefixes: $ReadOnlyArray<NodeAddressT>,
// The types are used to assign base cred to nodes based on their type. Node
// that the weight for each type may be overriden in the params.
+types: NodeAndEdgeTypes,
|};
/**
* Represents the timeline cred of a graph. This class wraps all the data
* needed to analyze and interpet cred (ie. it has the Graph and the cred
@ -94,20 +74,20 @@ export class TimelineCred {
_intervals: $ReadOnlyArray<Interval>;
_addressToCred: Map<NodeAddressT, $ReadOnlyArray<number>>;
_params: TimelineCredParameters;
_config: TimelineCredConfig;
_plugins: $ReadOnlyArray<PluginDeclaration>;
constructor(
graph: Graph,
intervals: $ReadOnlyArray<Interval>,
addressToCred: Map<NodeAddressT, $ReadOnlyArray<number>>,
params: TimelineCredParameters,
config: TimelineCredConfig
plugins: $ReadOnlyArray<PluginDeclaration>
) {
this._graph = graph;
this._intervals = intervals;
this._addressToCred = addressToCred;
this._params = params;
this._config = config;
this._plugins = plugins;
}
graph(): Graph {
@ -118,8 +98,8 @@ export class TimelineCred {
return this._params;
}
config(): TimelineCredConfig {
return this._config;
plugins(): $ReadOnlyArray<PluginDeclaration> {
return this._plugins;
}
/**
@ -129,7 +109,7 @@ export class TimelineCred {
* This returns a new TimelineCred; it does not modify the existing one.
*/
async reanalyze(newParams: TimelineCredParameters): Promise<TimelineCred> {
return await TimelineCred.compute(this._graph, newParams, this._config);
return await TimelineCred.compute(this._graph, newParams, this._plugins);
}
/**
@ -216,7 +196,7 @@ export class TimelineCred {
this._intervals,
filteredAddressToCred,
this._params,
this._config
this._plugins
);
}
@ -226,29 +206,32 @@ export class TimelineCred {
intervalsJSON: this._intervals,
credJSON: MapUtil.toObject(this._addressToCred),
paramsJSON: paramsToJSON(this._params),
configJSON: this._config,
pluginsJSON: this._plugins,
};
return toCompat(COMPAT_INFO, rawJSON);
}
static fromJSON(j: TimelineCredJSON): TimelineCred {
const json = fromCompat(COMPAT_INFO, j);
const {graphJSON, intervalsJSON, credJSON, paramsJSON, configJSON} = json;
const {graphJSON, intervalsJSON, credJSON, paramsJSON, pluginsJSON} = json;
const cred = MapUtil.fromObject(credJSON);
const graph = Graph.fromJSON(graphJSON);
const params = paramsFromJSON(paramsJSON);
return new TimelineCred(graph, intervalsJSON, cred, params, configJSON);
return new TimelineCred(graph, intervalsJSON, cred, params, pluginsJSON);
}
static async compute(
graph: Graph,
params: TimelineCredParameters,
config: TimelineCredConfig
plugins: $ReadOnlyArray<PluginDeclaration>
): Promise<TimelineCred> {
const nodeOrder = Array.from(graph.nodes()).map((x) => x.address);
const types = combineTypes(plugins);
const userTypes = [].concat(...plugins.map((x) => x.userTypes));
const scorePrefixes = userTypes.map((x) => x.prefix);
const distribution = await timelinePagerank(
graph,
config.types,
types,
params.weights,
params.intervalDecay,
params.alpha
@ -256,7 +239,7 @@ export class TimelineCred {
const cred = distributionToCred(
distribution,
nodeOrder,
config.scoreNodePrefixes
userTypes.map((x) => x.prefix)
);
const addressToCred = new Map();
for (let i = 0; i < nodeOrder.length; i++) {
@ -270,22 +253,22 @@ export class TimelineCred {
intervals,
addressToCred,
params,
config
plugins
);
return preliminaryCred.reduceSize({
typePrefixes: config.types.nodeTypes.map((x) => x.prefix),
typePrefixes: types.nodeTypes.map((x) => x.prefix),
nodesPerType: 100,
fullInclusionPrefixes: config.scoreNodePrefixes,
fullInclusionPrefixes: scorePrefixes,
});
}
}
const COMPAT_INFO = {type: "sourcecred/timelineCred", version: "0.4.0"};
const COMPAT_INFO = {type: "sourcecred/timelineCred", version: "0.5.0"};
export opaque type TimelineCredJSON = Compatible<{|
+graphJSON: GraphJSON,
+paramsJSON: ParamsJSON,
+configJSON: TimelineCredConfig,
+pluginsJSON: $ReadOnlyArray<PluginDeclaration>,
+credJSON: {[string]: $ReadOnlyArray<number>},
+intervalsJSON: $ReadOnlyArray<Interval>,
|}>;

View File

@ -4,8 +4,14 @@ import deepFreeze from "deep-freeze";
import {sum} from "d3-array";
import sortBy from "lodash.sortby";
import {utcWeek} from "d3-time";
import {NodeAddress, Graph, type NodeAddressT} from "../../core/graph";
import {TimelineCred, type TimelineCredConfig} from "./timelineCred";
import {
NodeAddress,
Graph,
type NodeAddressT,
EdgeAddress,
} from "../../core/graph";
import {TimelineCred} from "./timelineCred";
import {type PluginDeclaration} from "../pluginDeclaration";
import {defaultWeights} from "../weights";
import {type NodeType} from "../types";
@ -26,9 +32,13 @@ describe("src/analysis/timeline/timelineCred", () => {
description: "a foo",
};
const fooPrefix = fooType.prefix;
const credConfig: () => TimelineCredConfig = () => ({
scoreNodePrefixes: [userPrefix],
types: {nodeTypes: [userType, fooType], edgeTypes: []},
const plugin: PluginDeclaration = deepFreeze({
name: "foo",
nodePrefix: NodeAddress.empty,
edgePrefix: EdgeAddress.empty,
nodeTypes: [userType, fooType],
edgeTypes: [],
userTypes: [userType],
});
const users = [
["starter", (x) => Math.max(0, 20 - x)],
@ -75,13 +85,7 @@ describe("src/analysis/timeline/timelineCred", () => {
addressToCred.set(address, scores);
}
const params = {alpha: 0.05, intervalDecay: 0.5, weights: defaultWeights()};
return new TimelineCred(
graph,
intervals,
addressToCred,
params,
credConfig()
);
return new TimelineCred(graph, intervals, addressToCred, params, [plugin]);
}
it("JSON serialization works", () => {
@ -90,7 +94,7 @@ describe("src/analysis/timeline/timelineCred", () => {
const tc_ = TimelineCred.fromJSON(json);
expect(tc.graph()).toEqual(tc_.graph());
expect(tc.params()).toEqual(tc_.params());
expect(tc.config()).toEqual(tc_.config());
expect(tc.plugins()).toEqual(tc_.plugins());
expect(tc.credSortedNodes(NodeAddress.empty)).toEqual(
tc.credSortedNodes(NodeAddress.empty)
);

View File

@ -8,19 +8,19 @@ import {Graph} from "../core/graph";
import {loadGraph} from "../plugins/github/loadGraph";
import {
type TimelineCredParameters,
type TimelineCredConfig,
TimelineCred,
} from "../analysis/timeline/timelineCred";
import {type Project} from "../core/project";
import {setupProjectDirectory} from "../core/project_io";
import {loadDiscourse} from "../plugins/discourse/loadDiscourse";
import {type PluginDeclaration} from "../analysis/pluginDeclaration";
import * as NullUtil from "../util/null";
export type LoadOptions = {|
+project: Project,
+params: TimelineCredParameters,
+config: TimelineCredConfig,
+plugins: $ReadOnlyArray<PluginDeclaration>,
+sourcecredDirectory: string,
+githubToken: string | null,
+discourseKey: string | null,
@ -44,7 +44,7 @@ export async function load(
options: LoadOptions,
taskReporter: TaskReporter
): Promise<void> {
const {project, params, config, sourcecredDirectory, githubToken} = options;
const {project, params, plugins, sourcecredDirectory, githubToken} = options;
const loadTask = `load-${options.project.id}`;
taskReporter.start(loadTask);
const cacheDirectory = path.join(sourcecredDirectory, "cache");
@ -101,7 +101,7 @@ export async function load(
await fs.writeFile(graphFile, JSON.stringify(graph.toJSON()));
taskReporter.start("compute-cred");
const cred = await TimelineCred.compute(graph, params, config);
const cred = await TimelineCred.compute(graph, params, plugins);
const credJSON = cred.toJSON();
const credFile = path.join(projectDirectory, "cred.json");
await fs.writeFile(credFile, JSON.stringify(credJSON));

View File

@ -71,10 +71,7 @@ describe("api/load", () => {
weights.nodeManualWeights.set(NodeAddress.empty, 33);
// Deep freeze will freeze the weights, too
const params = deepFreeze({alpha: 0.05, intervalDecay: 0.5, weights});
const config = deepFreeze({
scoreNodePrefixes: [NodeAddress.empty],
types: {nodeTypes: [], edgeTypes: []},
});
const plugins = deepFreeze([]);
const example = () => {
const sourcecredDirectory = tmp.dirSync().name;
const taskReporter = new TestTaskReporter();
@ -82,7 +79,7 @@ describe("api/load", () => {
sourcecredDirectory,
githubToken,
params,
config,
plugins,
project,
discourseKey,
};
@ -145,7 +142,7 @@ describe("api/load", () => {
expect(timelineCredCompute).toHaveBeenCalledWith(
expect.anything(),
params,
config
plugins
);
expect(timelineCredCompute.mock.calls[0][0].equals(combinedGraph())).toBe(
true

View File

@ -1,13 +0,0 @@
// @flow
import deepFreeze from "deep-freeze";
import * as Github from "../plugins/github/declaration";
import type {TimelineCredConfig} from "../analysis/timeline/timelineCred";
export const DEFAULT_CRED_CONFIG: TimelineCredConfig = deepFreeze({
scoreNodePrefixes: [Github.userNodeType.prefix],
types: {
nodeTypes: Github.declaration.nodeTypes.slice(),
edgeTypes: Github.declaration.edgeTypes.slice(),
},
});

View File

@ -0,0 +1,9 @@
// @flow
import deepFreeze from "deep-freeze";
import {type PluginDeclaration} from "../analysis/pluginDeclaration";
import {declaration as githubDeclaration} from "../plugins/github/declaration";
export const DEFAULT_PLUGINS: $ReadOnlyArray<PluginDeclaration> = deepFreeze([
githubDeclaration,
]);

View File

@ -9,7 +9,7 @@ import {defaultWeights, fromJSON as weightsFromJSON} from "../analysis/weights";
import {load} from "../api/load";
import {specToProject} from "../plugins/github/specToProject";
import fs from "fs-extra";
import {DEFAULT_CRED_CONFIG} from "./defaultCredConfig";
import {DEFAULT_PLUGINS} from "./defaultPlugins";
function usage(print: (string) => void): void {
print(
@ -106,11 +106,11 @@ const loadCommand: Command = async (args, std) => {
projectSpecs.map((s) => specToProject(s, githubToken))
);
const params = {alpha: 0.05, intervalDecay: 0.5, weights};
const config = DEFAULT_CRED_CONFIG;
const plugins = DEFAULT_PLUGINS;
const optionses = projects.map((project) => ({
project,
params,
config,
plugins,
sourcecredDirectory: Common.sourcecredDirectory(),
githubToken,
discourseKey: Common.discourseKey(),

View File

@ -10,7 +10,7 @@ import loadCommand, {help} from "./load";
import type {LoadOptions} from "../api/load";
import {defaultWeights, toJSON as weightsToJSON} from "../analysis/weights";
import * as Common from "./common";
import {DEFAULT_CRED_CONFIG} from "./defaultCredConfig";
import {DEFAULT_PLUGINS} from "./defaultPlugins";
import {makeRepoId, stringToRepoId} from "../core/repoId";
@ -77,7 +77,7 @@ describe("cli/load", () => {
repoIds: [makeRepoId("foo", "bar")],
discourseServer: null,
},
config: DEFAULT_CRED_CONFIG,
plugins: DEFAULT_PLUGINS,
params: {alpha: 0.05, intervalDecay: 0.5, weights: defaultWeights()},
sourcecredDirectory: Common.sourcecredDirectory(),
githubToken: fakeGithubToken,
@ -103,7 +103,7 @@ describe("cli/load", () => {
discourseServer: null,
},
params: {alpha: 0.05, intervalDecay: 0.5, weights: defaultWeights()},
config: DEFAULT_CRED_CONFIG,
plugins: DEFAULT_PLUGINS,
sourcecredDirectory: Common.sourcecredDirectory(),
githubToken: fakeGithubToken,
discourseKey: fakeDiscourseKey,
@ -141,7 +141,7 @@ describe("cli/load", () => {
discourseServer: null,
},
params: {alpha: 0.05, intervalDecay: 0.5, weights},
config: DEFAULT_CRED_CONFIG,
plugins: DEFAULT_PLUGINS,
sourcecredDirectory: Common.sourcecredDirectory(),
githubToken: fakeGithubToken,
discourseKey: fakeDiscourseKey,

View File

@ -5,11 +5,7 @@ import {timeWeek} from "d3-time";
import type {Assets} from "../webutil/assets";
import {TimelineCredView} from "./TimelineCredView";
import {Graph, NodeAddress} from "../core/graph";
import {
type Interval,
TimelineCred,
type TimelineCredConfig,
} from "../analysis/timeline/timelineCred";
import {type Interval, TimelineCred} from "../analysis/timeline/timelineCred";
import {defaultWeights} from "../analysis/weights";
export default class TimelineCredViewInspectiontest extends React.Component<{|
@ -51,11 +47,7 @@ export default class TimelineCredViewInspectiontest extends React.Component<{|
addressToCred.set(address, scores);
}
const params = {alpha: 0.05, intervalDecay: 0.5, weights: defaultWeights()};
const config: TimelineCredConfig = {
scoreNodePrefixes: [NodeAddress.empty],
types: {nodeTypes: [], edgeTypes: []},
};
return new TimelineCred(graph, intervals, addressToCred, params, config);
return new TimelineCred(graph, intervals, addressToCred, params, []);
}
render() {

View File

@ -2,7 +2,10 @@
import React from "react";
import deepEqual from "lodash.isequal";
import {type PluginDeclaration} from "../analysis/pluginDeclaration";
import {
type PluginDeclaration,
combineTypes,
} from "../analysis/pluginDeclaration";
import {type Weights, copy as weightsCopy} from "../analysis/weights";
import {type NodeAddressT} from "../core/graph";
import {
@ -151,6 +154,7 @@ export class TimelineExplorer extends React.Component<Props, State> {
}
renderFilterSelect() {
const {nodeTypes} = combineTypes(this.state.timelineCred.plugins());
return (
<label>
<span style={{marginLeft: "5px"}}>Showing: </span>
@ -160,13 +164,11 @@ export class TimelineExplorer extends React.Component<Props, State> {
this.setState({selectedNodeTypePrefix: e.target.value})
}
>
{this.state.timelineCred
.config()
.types.nodeTypes.map(({prefix, pluralName}) => (
<option key={prefix} value={prefix}>
{pluralName}
</option>
))}
{nodeTypes.map(({prefix, pluralName}) => (
<option key={prefix} value={prefix}>
{pluralName}
</option>
))}
</select>
</label>
);