Compute the V1 output format from TimelineCred (#1782)

This commit builds on #1781, adding the logic for computing the first
output format from TimelineCred. See #1773 for context.

Test plan:
The logic is simple, but has a couple interesting edge cases. I've added
unit tests to cover them. `yarn test` passes.
This commit is contained in:
Dandelion Mané 2020-05-07 20:15:46 -07:00 committed by GitHub
parent b5fa5abb49
commit 610b9c4827
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 158 additions and 0 deletions

View File

@ -4,9 +4,14 @@
* This module defines a rich output format for cred scores, so that we can use
* it to drive UIs and data analysis.
*/
import * as NullUtil from "../util/null";
import {NodeAddress} from "../core/graph";
import type {Alias} from "../plugins/identity/alias";
import type {PluginDeclaration} from "./pluginDeclaration";
import type {TimestampMs} from "../util/timestamp";
import * as Timestamp from "../util/timestamp";
import {TimelineCred} from "./timeline/timelineCred";
import {nodeWeightEvaluator} from "../core/algorithm/weightEvaluator";
export type Index = number;
export type CredFlow = {|+forwards: number, +backwards: number|};
@ -42,6 +47,33 @@ export type OutputV1 = {|
+plugins: $ReadOnlyArray<PluginDeclaration>,
|};
export function fromTimelineCredAndPlugins(
tc: TimelineCred,
plugins: $ReadOnlyArray<PluginDeclaration>
): OutputV1 {
const {graph, weights} = tc.weightedGraph();
const nodeEvaluator = nodeWeightEvaluator(weights);
const orderedNodes = Array.from(graph.nodes()).map(
({description, address, timestampMs}) => {
const cred = NullUtil.get(tc.credNode(address)).total;
// In TimelineCred, a node with a null timestamp will never mint cred, because we don't
// know what period to mint it in.
// When we transition to CredRank, we should remove this check.
const minted = timestampMs == null ? 0 : nodeEvaluator(address);
const timestamp =
timestampMs == null ? null : Timestamp.fromNumber(timestampMs);
return {
address: NodeAddress.toParts(address),
cred,
minted,
description,
timestamp,
};
}
);
return {orderedNodes, plugins};
}
/**
* Extra data for Contributors. Note that each Contributor corresponds
* to a specific node in the graph, which is retrievable via the NodeIndex.

126
src/analysis/output.test.js Normal file
View File

@ -0,0 +1,126 @@
// @flow
import deepFreeze from "deep-freeze";
import {Graph, NodeAddress, EdgeAddress} from "../core/graph";
import {TimelineCred} from "./timeline/timelineCred";
import {defaultParams} from "./timeline/params";
import {nodeWeightEvaluator} from "../core/algorithm/weightEvaluator";
import {fromTimelineCredAndPlugins} from "./output";
describe("src/analysis/output", () => {
const nodeType = {
name: "node",
pluralName: "nodes",
prefix: NodeAddress.fromParts(["node"]),
defaultWeight: 2,
description: "a node",
};
const userType = {
name: "user",
pluralName: "users",
prefix: NodeAddress.fromParts(["user"]),
defaultWeight: 5,
description: "a user",
};
const plugin = deepFreeze({
name: "a plugin",
nodePrefix: NodeAddress.empty,
nodeTypes: [nodeType, userType],
edgePrefix: EdgeAddress.empty,
edgeTypes: [],
userTypes: [userType],
});
function example() {
const aNode = {
address: NodeAddress.fromParts(["node", "a"]),
description: "a node",
timestampMs: 123,
};
const bNode = {
address: NodeAddress.fromParts(["node", "b"]),
description: "b node",
timestampMs: 125,
};
const userNode = {
address: NodeAddress.fromParts(["user", "steven"]),
description: "a steven",
timestampMs: null,
};
const graph = new Graph().addNode(aNode).addNode(bNode).addNode(userNode);
const edgeWeights = new Map();
const nodeWeights = new Map()
.set(NodeAddress.empty, 5)
.set(bNode.address, 7);
const weights = {nodeWeights, edgeWeights};
const weightedGraph = {graph, weights};
const intervals = [
{startTimeMs: 0, endTimeMs: 1000},
{startTimeMs: 1000, endTimeMs: 2000},
];
const addressToCred = new Map()
.set(aNode.address, [1, 2])
.set(bNode.address, [2, 4])
.set(userNode.address, [5, 5]);
const params = defaultParams();
const plugins = [plugin];
const timelineCred = new TimelineCred(
weightedGraph,
intervals,
addressToCred,
params,
plugins
);
const output = fromTimelineCredAndPlugins(timelineCred, plugins);
return {aNode, bNode, userNode, intervals, timelineCred, output};
}
describe("output via fromTimelineCredAndPlugins", () => {
it("contains plugins", () => {
const {output} = example();
expect(output.plugins).toEqual([plugin]);
});
it("nodes have address, timestamp, description, and ordering from the graph", () => {
const {output, timelineCred} = example();
const nodes = Array.from(timelineCred.weightedGraph().graph.nodes());
expect(
output.orderedNodes.map((n) => ({
address: NodeAddress.fromParts(n.address),
description: n.description,
timestampMs: n.timestamp,
}))
).toEqual(nodes);
});
it("nodes' minted cred is computed correctly", () => {
// The minted cred is equal to the node weight, except in the special
// case where the timetsamp is null, in which case the minted cred is
// zero (per semantics of TimelineCred).
const {output, timelineCred} = example();
const {weights} = timelineCred.weightedGraph();
const nodeEvaluator = nodeWeightEvaluator(weights);
let foundEdgeCase = false;
for (const {address, minted, timestamp} of output.orderedNodes) {
const weight = nodeEvaluator(NodeAddress.fromParts(address));
if (timestamp == null && weight !== 0) {
foundEdgeCase = true;
expect(minted).toBe(0);
} else {
expect(minted).toBe(weight);
}
}
expect(foundEdgeCase).toBe(true);
});
it("nodes' cred is equal to the total cred across time slices", () => {
const {output, timelineCred} = example();
for (const {address, cred} of output.orderedNodes) {
const credNode = timelineCred.credNode(NodeAddress.fromParts(address));
if (credNode == null) {
throw new Error("Can't find node");
}
expect(cred).toEqual(credNode.total);
}
});
});
});