From fee071c031d8a79921a05aaa3b083253a60fb46e Mon Sep 17 00:00:00 2001 From: Robin van Boven <497556+Beanow@users.noreply.github.com> Date: Sat, 29 Feb 2020 05:56:56 -0700 Subject: [PATCH] Initiatives: define weights to allow Cred minting (#1674) Using a required type of before and after completion weight is a simple way to start minting Cred on Initiatives. It sets expectations by having both states defined in a version controlled file. --- .../initiativesDirectory.test.js.snap | 20 ++- src/plugins/initiatives/createGraph.js | 27 +++- src/plugins/initiatives/createGraph.test.js | 115 +++++++++++++++--- .../initiatives/example/initiative-A.json | 4 + .../initiatives/example/initiative-B.json | 4 + src/plugins/initiatives/initiative.js | 8 ++ .../initiatives/initiativesDirectory.js | 2 + .../initiatives/initiativesDirectory.test.js | 1 + 8 files changed, 154 insertions(+), 27 deletions(-) diff --git a/src/plugins/initiatives/__snapshots__/initiativesDirectory.test.js.snap b/src/plugins/initiatives/__snapshots__/initiativesDirectory.test.js.snap index bc1e0a7..90565ab 100644 --- a/src/plugins/initiatives/__snapshots__/initiativesDirectory.test.js.snap +++ b/src/plugins/initiatives/__snapshots__/initiativesDirectory.test.js.snap @@ -18,6 +18,10 @@ Map { ], "timestampIso": "2020-01-08T22:01:57.711Z", "title": "Initiative A", + "weight": Object { + "complete": 100, + "incomplete": 0, + }, }, "initiative-B.json" => Object { "champions": Array [ @@ -35,6 +39,10 @@ Map { ], "timestampIso": "2020-01-08T22:01:57.722Z", "title": "Initiative B", + "weight": Object { + "complete": 69, + "incomplete": 42, + }, }, } `; @@ -74,7 +82,11 @@ exports[`plugins/initiatives/initiativesDirectory loadDirectory should handle an \\"http://foo.bar/A/ref\\" ], \\"timestampMs\\": 1578520917711, - \\"title\\": \\"Initiative A\\" + \\"title\\": \\"Initiative A\\", + \\"weight\\": { + \\"complete\\": 100, + \\"incomplete\\": 0 + } }, { \\"champions\\": [ @@ -96,7 +108,11 @@ exports[`plugins/initiatives/initiativesDirectory loadDirectory should handle an \\"http://foo.bar/B/ref\\" ], \\"timestampMs\\": 1578520917722, - \\"title\\": \\"Initiative B\\" + \\"title\\": \\"Initiative B\\", + \\"weight\\": { + \\"complete\\": 69, + \\"incomplete\\": 42 + } } ]" `; diff --git a/src/plugins/initiatives/createGraph.js b/src/plugins/initiatives/createGraph.js index 6e36893..82d4831 100644 --- a/src/plugins/initiatives/createGraph.js +++ b/src/plugins/initiatives/createGraph.js @@ -1,7 +1,6 @@ // @flow import { - Graph, EdgeAddress, NodeAddress, type Edge, @@ -9,6 +8,9 @@ import { type EdgeAddressT, type NodeAddressT, } from "../../core/graph"; +import {type WeightedGraph as WeightedGraphT} from "../../core/weightedGraph"; +import * as WeightedGraph from "../../core/weightedGraph"; +import type {NodeWeight} from "../../core/weights"; import type {ReferenceDetector, URL} from "../../core/references"; import type {Initiative, InitiativeRepository} from "./initiative"; import {addressFromId} from "./initiative"; @@ -35,6 +37,13 @@ function initiativeNode(initiative: Initiative): Node { }; } +export function initiativeWeight(initiative: Initiative): ?NodeWeight { + if (!initiative.weight) return; + return initiative.completed + ? initiative.weight.complete + : initiative.weight.incomplete; +} + type EdgeFactoryT = (initiative: Initiative, other: NodeAddressT) => Edge; function edgeFactory( @@ -63,15 +72,21 @@ const referenceEdge = edgeFactory(referencesEdgeType.prefix, true); const contributionEdge = edgeFactory(contributesToEdgeType.prefix, false); const championEdge = edgeFactory(championsEdgeType.prefix, false); -export function createGraph( +export function createWeightedGraph( repo: InitiativeRepository, refs: ReferenceDetector -): Graph { - const graph = new Graph(); +): WeightedGraphT { + const wg = WeightedGraph.empty(); + const {graph, weights} = wg; for (const initiative of repo.initiatives()) { // Adds the Initiative node. - graph.addNode(initiativeNode(initiative)); + const node = initiativeNode(initiative); + const weight = initiativeWeight(initiative); + graph.addNode(node); + if (weight) { + weights.nodeWeights.set(node.address, weight); + } // Generic approach to adding edges when the reference detector has a hit. const edgeHandler = ( @@ -92,5 +107,5 @@ export function createGraph( edgeHandler(initiative.champions, championEdge); } - return graph; + return wg; } diff --git a/src/plugins/initiatives/createGraph.test.js b/src/plugins/initiatives/createGraph.test.js index 927bf79..2d8b830 100644 --- a/src/plugins/initiatives/createGraph.test.js +++ b/src/plugins/initiatives/createGraph.test.js @@ -6,10 +6,11 @@ import { type EdgeAddressT, type NodeAddressT, } from "../../core/graph"; +import * as Weights from "../../core/weights"; import type {ReferenceDetector, URL} from "../../core/references"; import type {Initiative, InitiativeRepository} from "./initiative"; import {createId, addressFromId} from "./initiative"; -import {createGraph} from "./createGraph"; +import {createWeightedGraph, initiativeWeight} from "./createGraph"; import { initiativeNodeType, dependsOnEdgeType, @@ -18,6 +19,20 @@ import { championsEdgeType, } from "./declaration"; +function _createInitiative(overrides?: $Shape): Initiative { + return { + id: createId("UNSET_SUBTYPE", "42"), + title: "Unset test initiative", + timestampMs: 123, + completed: false, + dependencies: [], + references: [], + contributions: [], + champions: [], + ...overrides, + }; +} + class MockInitiativeRepository implements InitiativeRepository { _counter: number; _initiatives: Initiative[]; @@ -31,17 +46,12 @@ class MockInitiativeRepository implements InitiativeRepository { const num = this._counter; this._counter++; - const initiative: Initiative = { + const initiative = _createInitiative({ id: createId("TEST_SUBTYPE", String(num)), title: `Example Initiative ${num}`, timestampMs: 400 + num, - completed: false, - dependencies: [], - references: [], - contributions: [], - champions: [], ...shape, - }; + }); this._initiatives.push(initiative); return initiative; @@ -107,7 +117,55 @@ const contributionEdgeAddress = edgeAddress(contributesToEdgeType.prefix); const championEdgeAddress = edgeAddress(championsEdgeType.prefix); describe("plugins/initiatives/createGraph", () => { - describe("createGraph", () => { + describe("initiativeWeight", () => { + it("should be falsy when the initiative has no weight set", () => { + // Given + const initiative = _createInitiative({ + id: createId("TEST_INITIATIVE_WEIGHTS", "41"), + title: "No weight set", + }); + + // When + const maybeWeight = initiativeWeight(initiative); + + // Then + expect(maybeWeight).toBeFalsy(); + }); + + it("should use the first weight when not completed", () => { + // Given + const initiative = _createInitiative({ + id: createId("TEST_INITIATIVE_WEIGHTS", "41"), + title: "Weights set, not completed", + completed: false, + weight: {incomplete: 222, complete: 333}, + }); + + // When + const maybeWeight = initiativeWeight(initiative); + + // Then + expect(maybeWeight).toEqual(222); + }); + + it("should use the second weight when completed", () => { + // Given + const initiative = _createInitiative({ + id: createId("TEST_INITIATIVE_WEIGHTS", "41"), + title: "Weights set, completed", + completed: true, + weight: {incomplete: 222, complete: 333}, + }); + + // When + const maybeWeight = initiativeWeight(initiative); + + // Then + expect(maybeWeight).toEqual(333); + }); + }); + + describe("createWeightedGraph", () => { it("should add initiative nodes to the graph", () => { // Given const {repo, refs} = example(); @@ -115,7 +173,7 @@ describe("plugins/initiatives/createGraph", () => { repo.addInitiative(); // When - const graph = createGraph(repo, refs); + const {graph, weights} = createWeightedGraph(repo, refs); // Then const nodes = Array.from( @@ -133,6 +191,25 @@ describe("plugins/initiatives/createGraph", () => { address: testInitiativeAddress(2), }, ]); + expect(weights).toEqual(Weights.empty()); + }); + + it("should add node weights for initiatives with weights", () => { + // Given + const {repo, refs} = example(); + repo.addInitiative({weight: {incomplete: 360, complete: 420}}); + repo.addInitiative({weight: {incomplete: 42, complete: 69}}); + repo.addInitiative({ + weight: {incomplete: 42, complete: 69}, + completed: true, + }); + + // When + const {weights} = createWeightedGraph(repo, refs); + + // Then + expect(weights.edgeWeights.size).toEqual(0); + expect([...weights.nodeWeights.values()]).toEqual([360, 42, 69]); }); it("should add initiative file urls to the description", () => { @@ -145,7 +222,7 @@ describe("plugins/initiatives/createGraph", () => { repo.addInitiative({id}); // When - const graph = createGraph(repo, refs); + const {graph} = createWeightedGraph(repo, refs); // Then const node = graph.node(addres); @@ -163,7 +240,7 @@ describe("plugins/initiatives/createGraph", () => { }); // When - createGraph(repo, refs); + createWeightedGraph(repo, refs); // Then expect(refs.addressFromUrl).toHaveBeenCalledWith( @@ -179,7 +256,7 @@ describe("plugins/initiatives/createGraph", () => { }); // When - createGraph(repo, refs); + createWeightedGraph(repo, refs); // Then expect(refs.addressFromUrl).toHaveBeenCalledWith( @@ -195,7 +272,7 @@ describe("plugins/initiatives/createGraph", () => { }); // When - createGraph(repo, refs); + createWeightedGraph(repo, refs); // Then expect(refs.addressFromUrl).toHaveBeenCalledWith( @@ -211,7 +288,7 @@ describe("plugins/initiatives/createGraph", () => { }); // When - createGraph(repo, refs); + createWeightedGraph(repo, refs); // Then expect(refs.addressFromUrl).toHaveBeenCalledWith( @@ -230,7 +307,7 @@ describe("plugins/initiatives/createGraph", () => { }); // When - const graph = createGraph(repo, refs); + const {graph} = createWeightedGraph(repo, refs); // Then const dependencies = Array.from( @@ -261,7 +338,7 @@ describe("plugins/initiatives/createGraph", () => { }); // When - const graph = createGraph(repo, refs); + const {graph} = createWeightedGraph(repo, refs); // Then const references = Array.from( @@ -292,7 +369,7 @@ describe("plugins/initiatives/createGraph", () => { }); // When - const graph = createGraph(repo, refs); + const {graph} = createWeightedGraph(repo, refs); // Then const contributions = Array.from( @@ -323,7 +400,7 @@ describe("plugins/initiatives/createGraph", () => { }); // When - const graph = createGraph(repo, refs); + const {graph} = createWeightedGraph(repo, refs); // Then const champions = Array.from( diff --git a/src/plugins/initiatives/example/initiative-A.json b/src/plugins/initiatives/example/initiative-A.json index 561e143..cf950f1 100644 --- a/src/plugins/initiatives/example/initiative-A.json +++ b/src/plugins/initiatives/example/initiative-A.json @@ -6,6 +6,10 @@ { "title": "Initiative A", "timestampIso": "2020-01-08T22:01:57.711Z", + "weight": { + "incomplete": 0, + "complete": 100 + }, "completed": true, "champions": ["http://foo.bar/A/champ"], "contributions": ["http://foo.bar/A/contrib"], diff --git a/src/plugins/initiatives/example/initiative-B.json b/src/plugins/initiatives/example/initiative-B.json index 98f57b0..af1fc9b 100644 --- a/src/plugins/initiatives/example/initiative-B.json +++ b/src/plugins/initiatives/example/initiative-B.json @@ -6,6 +6,10 @@ { "title": "Initiative B", "timestampIso": "2020-01-08T22:01:57.722Z", + "weight": { + "incomplete": 42, + "complete": 69 + }, "completed": false, "champions": ["http://foo.bar/B/champ"], "contributions": ["http://foo.bar/B/contrib"], diff --git a/src/plugins/initiatives/initiative.js b/src/plugins/initiatives/initiative.js index a730f48..2046e97 100644 --- a/src/plugins/initiatives/initiative.js +++ b/src/plugins/initiatives/initiative.js @@ -1,6 +1,7 @@ // @flow import {type NodeAddressT, NodeAddress} from "../../core/graph"; +import {type NodeWeight} from "../../core/weights"; import {initiativeNodeType} from "./declaration"; export type URL = string; @@ -21,6 +22,12 @@ export function addressFromId(id: InitiativeId): NodeAddressT { return NodeAddress.append(initiativeNodeType.prefix, ...id); } +// A before completion and after completion weight for Initiatives. +export type InitiativeWeight = {| + +incomplete: NodeWeight, + +complete: NodeWeight, +|}; + /** * An intermediate representation of an Initiative. * @@ -39,6 +46,7 @@ export type Initiative = {| +id: InitiativeId, +title: string, +timestampMs: number, + +weight?: InitiativeWeight, +completed: boolean, +dependencies: $ReadOnlyArray, +references: $ReadOnlyArray, diff --git a/src/plugins/initiatives/initiativesDirectory.js b/src/plugins/initiatives/initiativesDirectory.js index 5063ffb..59bde22 100644 --- a/src/plugins/initiatives/initiativesDirectory.js +++ b/src/plugins/initiatives/initiativesDirectory.js @@ -13,6 +13,7 @@ import { } from "../../core/references"; import { type Initiative, + type InitiativeWeight, type InitiativeId, type InitiativeRepository, type URL, @@ -82,6 +83,7 @@ export async function loadDirectory( export type InitiativeFile = {| +title: string, +timestampIso: ISOTimestamp, + +weight: InitiativeWeight, +completed: boolean, +dependencies: $ReadOnlyArray, +references: $ReadOnlyArray, diff --git a/src/plugins/initiatives/initiativesDirectory.test.js b/src/plugins/initiatives/initiativesDirectory.test.js index 98f0351..05d9efe 100644 --- a/src/plugins/initiatives/initiativesDirectory.test.js +++ b/src/plugins/initiatives/initiativesDirectory.test.js @@ -26,6 +26,7 @@ import { const exampleInitiativeFile = (): InitiativeFile => ({ title: "Sample initiative", timestampIso: ("2020-01-08T22:01:57.766Z": any), + weight: {incomplete: 360, complete: 420}, completed: false, champions: ["http://foo.bar/champ"], contributions: ["http://foo.bar/contrib"],