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", "timestampIso": "2020-01-08T22:01:57.711Z",
"title": "Initiative A", "title": "Initiative A",
"weight": Object {
"complete": 100,
"incomplete": 0,
},
}, },
"initiative-B.json" => Object { "initiative-B.json" => Object {
"champions": Array [ "champions": Array [
@ -35,6 +39,10 @@ Map {
], ],
"timestampIso": "2020-01-08T22:01:57.722Z", "timestampIso": "2020-01-08T22:01:57.722Z",
"title": "Initiative B", "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\\" \\"http://foo.bar/A/ref\\"
], ],
\\"timestampMs\\": 1578520917711, \\"timestampMs\\": 1578520917711,
\\"title\\": \\"Initiative A\\" \\"title\\": \\"Initiative A\\",
\\"weight\\": {
\\"complete\\": 100,
\\"incomplete\\": 0
}
}, },
{ {
\\"champions\\": [ \\"champions\\": [
@ -96,7 +108,11 @@ exports[`plugins/initiatives/initiativesDirectory loadDirectory should handle an
\\"http://foo.bar/B/ref\\" \\"http://foo.bar/B/ref\\"
], ],
\\"timestampMs\\": 1578520917722, \\"timestampMs\\": 1578520917722,
\\"title\\": \\"Initiative B\\" \\"title\\": \\"Initiative B\\",
\\"weight\\": {
\\"complete\\": 69,
\\"incomplete\\": 42
}
} }
]" ]"
`; `;

View File

@ -1,7 +1,6 @@
// @flow // @flow
import { import {
Graph,
EdgeAddress, EdgeAddress,
NodeAddress, NodeAddress,
type Edge, type Edge,
@ -9,6 +8,9 @@ import {
type EdgeAddressT, type EdgeAddressT,
type NodeAddressT, type NodeAddressT,
} from "../../core/graph"; } 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 {ReferenceDetector, URL} from "../../core/references";
import type {Initiative, InitiativeRepository} from "./initiative"; import type {Initiative, InitiativeRepository} from "./initiative";
import {addressFromId} 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; type EdgeFactoryT = (initiative: Initiative, other: NodeAddressT) => Edge;
function edgeFactory( function edgeFactory(
@ -63,15 +72,21 @@ const referenceEdge = edgeFactory(referencesEdgeType.prefix, true);
const contributionEdge = edgeFactory(contributesToEdgeType.prefix, false); const contributionEdge = edgeFactory(contributesToEdgeType.prefix, false);
const championEdge = edgeFactory(championsEdgeType.prefix, false); const championEdge = edgeFactory(championsEdgeType.prefix, false);
export function createGraph( export function createWeightedGraph(
repo: InitiativeRepository, repo: InitiativeRepository,
refs: ReferenceDetector refs: ReferenceDetector
): Graph { ): WeightedGraphT {
const graph = new Graph(); const wg = WeightedGraph.empty();
const {graph, weights} = wg;
for (const initiative of repo.initiatives()) { for (const initiative of repo.initiatives()) {
// Adds the Initiative node. // 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. // Generic approach to adding edges when the reference detector has a hit.
const edgeHandler = ( const edgeHandler = (
@ -92,5 +107,5 @@ export function createGraph(
edgeHandler(initiative.champions, championEdge); edgeHandler(initiative.champions, championEdge);
} }
return graph; return wg;
} }

View File

@ -6,10 +6,11 @@ import {
type EdgeAddressT, type EdgeAddressT,
type NodeAddressT, type NodeAddressT,
} from "../../core/graph"; } from "../../core/graph";
import * as Weights from "../../core/weights";
import type {ReferenceDetector, URL} from "../../core/references"; import type {ReferenceDetector, URL} from "../../core/references";
import type {Initiative, InitiativeRepository} from "./initiative"; import type {Initiative, InitiativeRepository} from "./initiative";
import {createId, addressFromId} from "./initiative"; import {createId, addressFromId} from "./initiative";
import {createGraph} from "./createGraph"; import {createWeightedGraph, initiativeWeight} from "./createGraph";
import { import {
initiativeNodeType, initiativeNodeType,
dependsOnEdgeType, dependsOnEdgeType,
@ -18,6 +19,20 @@ import {
championsEdgeType, championsEdgeType,
} from "./declaration"; } 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 { class MockInitiativeRepository implements InitiativeRepository {
_counter: number; _counter: number;
_initiatives: Initiative[]; _initiatives: Initiative[];
@ -31,17 +46,12 @@ class MockInitiativeRepository implements InitiativeRepository {
const num = this._counter; const num = this._counter;
this._counter++; this._counter++;
const initiative: Initiative = { const initiative = _createInitiative({
id: createId("TEST_SUBTYPE", String(num)), id: createId("TEST_SUBTYPE", String(num)),
title: `Example Initiative ${num}`, title: `Example Initiative ${num}`,
timestampMs: 400 + num, timestampMs: 400 + num,
completed: false,
dependencies: [],
references: [],
contributions: [],
champions: [],
...shape, ...shape,
}; });
this._initiatives.push(initiative); this._initiatives.push(initiative);
return initiative; return initiative;
@ -107,7 +117,55 @@ const contributionEdgeAddress = edgeAddress(contributesToEdgeType.prefix);
const championEdgeAddress = edgeAddress(championsEdgeType.prefix); const championEdgeAddress = edgeAddress(championsEdgeType.prefix);
describe("plugins/initiatives/createGraph", () => { 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", () => { it("should add initiative nodes to the graph", () => {
// Given // Given
const {repo, refs} = example(); const {repo, refs} = example();
@ -115,7 +173,7 @@ describe("plugins/initiatives/createGraph", () => {
repo.addInitiative(); repo.addInitiative();
// When // When
const graph = createGraph(repo, refs); const {graph, weights} = createWeightedGraph(repo, refs);
// Then // Then
const nodes = Array.from( const nodes = Array.from(
@ -133,6 +191,25 @@ describe("plugins/initiatives/createGraph", () => {
address: testInitiativeAddress(2), 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", () => { it("should add initiative file urls to the description", () => {
@ -145,7 +222,7 @@ describe("plugins/initiatives/createGraph", () => {
repo.addInitiative({id}); repo.addInitiative({id});
// When // When
const graph = createGraph(repo, refs); const {graph} = createWeightedGraph(repo, refs);
// Then // Then
const node = graph.node(addres); const node = graph.node(addres);
@ -163,7 +240,7 @@ describe("plugins/initiatives/createGraph", () => {
}); });
// When // When
createGraph(repo, refs); createWeightedGraph(repo, refs);
// Then // Then
expect(refs.addressFromUrl).toHaveBeenCalledWith( expect(refs.addressFromUrl).toHaveBeenCalledWith(
@ -179,7 +256,7 @@ describe("plugins/initiatives/createGraph", () => {
}); });
// When // When
createGraph(repo, refs); createWeightedGraph(repo, refs);
// Then // Then
expect(refs.addressFromUrl).toHaveBeenCalledWith( expect(refs.addressFromUrl).toHaveBeenCalledWith(
@ -195,7 +272,7 @@ describe("plugins/initiatives/createGraph", () => {
}); });
// When // When
createGraph(repo, refs); createWeightedGraph(repo, refs);
// Then // Then
expect(refs.addressFromUrl).toHaveBeenCalledWith( expect(refs.addressFromUrl).toHaveBeenCalledWith(
@ -211,7 +288,7 @@ describe("plugins/initiatives/createGraph", () => {
}); });
// When // When
createGraph(repo, refs); createWeightedGraph(repo, refs);
// Then // Then
expect(refs.addressFromUrl).toHaveBeenCalledWith( expect(refs.addressFromUrl).toHaveBeenCalledWith(
@ -230,7 +307,7 @@ describe("plugins/initiatives/createGraph", () => {
}); });
// When // When
const graph = createGraph(repo, refs); const {graph} = createWeightedGraph(repo, refs);
// Then // Then
const dependencies = Array.from( const dependencies = Array.from(
@ -261,7 +338,7 @@ describe("plugins/initiatives/createGraph", () => {
}); });
// When // When
const graph = createGraph(repo, refs); const {graph} = createWeightedGraph(repo, refs);
// Then // Then
const references = Array.from( const references = Array.from(
@ -292,7 +369,7 @@ describe("plugins/initiatives/createGraph", () => {
}); });
// When // When
const graph = createGraph(repo, refs); const {graph} = createWeightedGraph(repo, refs);
// Then // Then
const contributions = Array.from( const contributions = Array.from(
@ -323,7 +400,7 @@ describe("plugins/initiatives/createGraph", () => {
}); });
// When // When
const graph = createGraph(repo, refs); const {graph} = createWeightedGraph(repo, refs);
// Then // Then
const champions = Array.from( const champions = Array.from(

View File

@ -6,6 +6,10 @@
{ {
"title": "Initiative A", "title": "Initiative A",
"timestampIso": "2020-01-08T22:01:57.711Z", "timestampIso": "2020-01-08T22:01:57.711Z",
"weight": {
"incomplete": 0,
"complete": 100
},
"completed": true, "completed": true,
"champions": ["http://foo.bar/A/champ"], "champions": ["http://foo.bar/A/champ"],
"contributions": ["http://foo.bar/A/contrib"], "contributions": ["http://foo.bar/A/contrib"],

View File

@ -6,6 +6,10 @@
{ {
"title": "Initiative B", "title": "Initiative B",
"timestampIso": "2020-01-08T22:01:57.722Z", "timestampIso": "2020-01-08T22:01:57.722Z",
"weight": {
"incomplete": 42,
"complete": 69
},
"completed": false, "completed": false,
"champions": ["http://foo.bar/B/champ"], "champions": ["http://foo.bar/B/champ"],
"contributions": ["http://foo.bar/B/contrib"], "contributions": ["http://foo.bar/B/contrib"],

View File

@ -1,6 +1,7 @@
// @flow // @flow
import {type NodeAddressT, NodeAddress} from "../../core/graph"; import {type NodeAddressT, NodeAddress} from "../../core/graph";
import {type NodeWeight} from "../../core/weights";
import {initiativeNodeType} from "./declaration"; import {initiativeNodeType} from "./declaration";
export type URL = string; export type URL = string;
@ -21,6 +22,12 @@ export function addressFromId(id: InitiativeId): NodeAddressT {
return NodeAddress.append(initiativeNodeType.prefix, ...id); 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. * An intermediate representation of an Initiative.
* *
@ -39,6 +46,7 @@ export type Initiative = {|
+id: InitiativeId, +id: InitiativeId,
+title: string, +title: string,
+timestampMs: number, +timestampMs: number,
+weight?: InitiativeWeight,
+completed: boolean, +completed: boolean,
+dependencies: $ReadOnlyArray<URL>, +dependencies: $ReadOnlyArray<URL>,
+references: $ReadOnlyArray<URL>, +references: $ReadOnlyArray<URL>,

View File

@ -13,6 +13,7 @@ import {
} from "../../core/references"; } from "../../core/references";
import { import {
type Initiative, type Initiative,
type InitiativeWeight,
type InitiativeId, type InitiativeId,
type InitiativeRepository, type InitiativeRepository,
type URL, type URL,
@ -82,6 +83,7 @@ export async function loadDirectory(
export type InitiativeFile = {| export type InitiativeFile = {|
+title: string, +title: string,
+timestampIso: ISOTimestamp, +timestampIso: ISOTimestamp,
+weight: InitiativeWeight,
+completed: boolean, +completed: boolean,
+dependencies: $ReadOnlyArray<URL>, +dependencies: $ReadOnlyArray<URL>,
+references: $ReadOnlyArray<URL>, +references: $ReadOnlyArray<URL>,

View File

@ -26,6 +26,7 @@ import {
const exampleInitiativeFile = (): InitiativeFile => ({ const exampleInitiativeFile = (): InitiativeFile => ({
title: "Sample initiative", title: "Sample initiative",
timestampIso: ("2020-01-08T22:01:57.766Z": any), timestampIso: ("2020-01-08T22:01:57.766Z": any),
weight: {incomplete: 360, complete: 420},
completed: false, completed: false,
champions: ["http://foo.bar/champ"], champions: ["http://foo.bar/champ"],
contributions: ["http://foo.bar/contrib"], contributions: ["http://foo.bar/contrib"],