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.
This commit is contained in:
Robin van Boven 2020-02-29 05:56:56 -07:00 committed by GitHub
parent 660f607011
commit fee071c031
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 154 additions and 27 deletions

View File

@ -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
}
}
]"
`;

View File

@ -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;
}

View File

@ -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>): 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(

View File

@ -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"],

View File

@ -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"],

View File

@ -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<URL>,
+references: $ReadOnlyArray<URL>,

View File

@ -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<URL>,
+references: $ReadOnlyArray<URL>,

View File

@ -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"],