From 610b9c4827005875019c389650c53180fec7aa92 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dandelion=20Man=C3=A9?= Date: Thu, 7 May 2020 20:15:46 -0700 Subject: [PATCH] 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. --- src/analysis/output.js | 32 +++++++++ src/analysis/output.test.js | 126 ++++++++++++++++++++++++++++++++++++ 2 files changed, 158 insertions(+) create mode 100644 src/analysis/output.test.js diff --git a/src/analysis/output.js b/src/analysis/output.js index a701079..23a772d 100644 --- a/src/analysis/output.js +++ b/src/analysis/output.js @@ -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, |}; +export function fromTimelineCredAndPlugins( + tc: TimelineCred, + plugins: $ReadOnlyArray +): 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. diff --git a/src/analysis/output.test.js b/src/analysis/output.test.js new file mode 100644 index 0000000..9945c74 --- /dev/null +++ b/src/analysis/output.test.js @@ -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); + } + }); + }); +});