Initial data model for the Odyssey plugin (#1134)
This commit puts in a basic data model for the Odyssey plugin. It's built around the `OdysseyInstance` class, which is basically a Graph that keeps around descriptions for every node, and ensures that nodes/edges are typed in accordance with the Odyssey plugin declaration. In the future, I want to enable instances to declare their own node/edge types, in which case the instance will assume responsibility for tracking and serializing the types. To make the Odyssey plugin a 'proper plugin', I've also added a plugin declaration, as well as analysis and explorer adapters. I haven't decided exactly where data for Odyssey instances should be stored, so for now the plugin adapters always return an example instance which is based on our experience at the Odyssey hackathon. Test plan: The instance has unit tests for its logic. If you want to see what the plugin looks like right now when it's integrated, you can apply the following diff, and then load the prototype. It will contain Odyssey nodes (find them using the node type dropdown). Note that without a seed vector to move cred back to the values/artifacts, the cred distribution in the Odyssey subgraph is degenerate; the users are all sinks and have postiive cred scores, but all the other nodes converge to 0 cred. diff --git a/src/homepage/homepageExplorer.js b/src/homepage/homepageExplorer.js index cae4548..48987f1 100644 --- a/src/homepage/homepageExplorer.js +++ b/src/homepage/homepageExplorer.js @@ -6,6 +6,7 @@ import type {Assets} from "../webutil/assets"; import {StaticExplorerAdapterSet} from "../explorer/adapters/explorerAdapterSet"; import {StaticExplorerAdapter as GithubAdapter} from "../plugins/github/explorerAdapter"; import {StaticExplorerAdapter as GitAdapter} from "../plugins/git/explorerAdapter"; +import {StaticExplorerAdapter as OdysseyAdapter} from "../plugins/odyssey/explorerAdapter"; import {GithubGitGateway} from "../plugins/github/githubGitGateway"; import {AppPage} from "../explorer/App"; import type {RepoId} from "../core/repoId"; @@ -14,6 +15,7 @@ function homepageStaticAdapters(): StaticExplorerAdapterSet { return new StaticExplorerAdapterSet([ new GithubAdapter(), new GitAdapter(new GithubGitGateway()), + new OdysseyAdapter(), ]); }
This commit is contained in:
parent
79017a477b
commit
9231085185
|
@ -0,0 +1,20 @@
|
|||
// @flow
|
||||
|
||||
import {Graph} from "../../core/graph";
|
||||
import type {RepoId} from "../../core/repoId";
|
||||
import type {IAnalysisAdapter} from "../../analysis/analysisAdapter";
|
||||
import {hackathonExample} from "./example";
|
||||
import {declaration} from "./declaration";
|
||||
|
||||
export class AnalysisAdapter implements IAnalysisAdapter {
|
||||
declaration() {
|
||||
return declaration;
|
||||
}
|
||||
// TODO(@decentralion): Enable loading graphs other than the hackathon example.
|
||||
async load(
|
||||
_unused_sourcecredDirectory: string,
|
||||
_unused_repoId: RepoId
|
||||
): Promise<Graph> {
|
||||
return hackathonExample().graph();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,83 @@
|
|||
// @flow
|
||||
|
||||
import {EdgeAddress, NodeAddress} from "../../core/graph";
|
||||
|
||||
import type {PluginDeclaration} from "../../analysis/pluginDeclaration";
|
||||
import type {NodeType} from "../../analysis/types";
|
||||
|
||||
export type OdysseyNodeTypeIdentifier =
|
||||
| "ARTIFACT"
|
||||
| "CONTRIBUTION"
|
||||
| "VALUE"
|
||||
| "PERSON";
|
||||
|
||||
export type OdysseyEdgeTypeIdentifier = "DEPENDS_ON";
|
||||
|
||||
export function isOdysseyNodeTypeIdentifier(
|
||||
n: OdysseyNodeTypeIdentifier
|
||||
): boolean {
|
||||
return ["ARTIFACT", "CONTRIBUTION", "VALUE", "PERSON"].indexOf(n) !== -1;
|
||||
}
|
||||
|
||||
export const NODE_PREFIX = NodeAddress.fromParts(["sourcecred", "odyssey"]);
|
||||
|
||||
export function isOdysseyEdgeTypeIdentifier(x: string): boolean {
|
||||
return x === "DEPENDS_ON";
|
||||
}
|
||||
export const EDGE_PREFIX = EdgeAddress.fromParts(["sourcecred", "odyssey"]);
|
||||
|
||||
const artifactNodeType: NodeType = Object.freeze({
|
||||
name: "Artifact",
|
||||
pluralName: "Artifacts",
|
||||
prefix: NodeAddress.append(NODE_PREFIX, "ARTIFACT"),
|
||||
defaultWeight: 2,
|
||||
description:
|
||||
"Represents a durably valuable piece of a project, e.g. a major subcomponent.",
|
||||
});
|
||||
|
||||
const contributionNodeType: NodeType = Object.freeze({
|
||||
name: "Contribution",
|
||||
pluralName: "Contributions",
|
||||
prefix: NodeAddress.append(NODE_PREFIX, "CONTRIBUTION"),
|
||||
defaultWeight: 1,
|
||||
description:
|
||||
"Represents any specific work or labor that went into a project.",
|
||||
});
|
||||
|
||||
const valueNodeType: NodeType = Object.freeze({
|
||||
name: "Value",
|
||||
pluralName: "Values",
|
||||
prefix: NodeAddress.append(NODE_PREFIX, "VALUE"),
|
||||
defaultWeight: 4,
|
||||
description: "Represents a high-level value of the project.",
|
||||
});
|
||||
|
||||
const personNodeType: NodeType = Object.freeze({
|
||||
name: "Person",
|
||||
pluralName: "People",
|
||||
prefix: NodeAddress.append(NODE_PREFIX, "PERSON"),
|
||||
defaultWeight: 1,
|
||||
description: "Represents an individual contributor.",
|
||||
});
|
||||
|
||||
const dependsOnEdgeType = Object.freeze({
|
||||
forwardName: "depends on",
|
||||
backwardName: "is depended on by",
|
||||
prefix: EdgeAddress.append(EDGE_PREFIX, "DEPENDS_ON"),
|
||||
defaultForwardWeight: 1,
|
||||
defaultBackwardWeight: 0,
|
||||
description: "Generic edge for flowing credit in the Odyssey plugin",
|
||||
});
|
||||
|
||||
export const declaration: PluginDeclaration = Object.freeze({
|
||||
name: "Odyssey",
|
||||
nodePrefix: NODE_PREFIX,
|
||||
edgePrefix: EDGE_PREFIX,
|
||||
nodeTypes: [
|
||||
contributionNodeType,
|
||||
valueNodeType,
|
||||
personNodeType,
|
||||
artifactNodeType,
|
||||
],
|
||||
edgeTypes: [dependsOnEdgeType],
|
||||
});
|
|
@ -0,0 +1,114 @@
|
|||
// @flow
|
||||
|
||||
import {OdysseyInstance, type Node} from "./instance";
|
||||
|
||||
/**
|
||||
* An example Odyssey instance, based on work at the Odyssey Hackathon.
|
||||
*/
|
||||
export function hackathonExample(): OdysseyInstance {
|
||||
// Define the types of nodes allowed in our instance
|
||||
const instance = new OdysseyInstance();
|
||||
|
||||
// define our values for the hackathon
|
||||
const logistics = instance.addNode("VALUE", "logistics");
|
||||
const design = instance.addNode("VALUE", "design");
|
||||
const narrative = instance.addNode("VALUE", "narrative");
|
||||
const prototype = instance.addNode("VALUE", "prototype");
|
||||
const outreach = instance.addNode("VALUE", "outreach");
|
||||
|
||||
// the cast of characters
|
||||
const dl = instance.addNode("PERSON", "dandelion");
|
||||
const mz = instance.addNode("PERSON", "z zargham");
|
||||
const irene = instance.addNode("PERSON", "irene");
|
||||
const max = instance.addNode("PERSON", "max");
|
||||
const dennis = instance.addNode("PERSON", "dennis");
|
||||
const jonathan = instance.addNode("PERSON", "jonathan");
|
||||
const lb = instance.addNode("PERSON", "lb");
|
||||
const brian = instance.addNode("PERSON", "brian");
|
||||
const sarah = instance.addNode("PERSON", "sarah");
|
||||
const jmnemo = instance.addNode("PERSON", "@jmnemo");
|
||||
const talbott = instance.addNode("PERSON", "jonathan talbott");
|
||||
const agata = instance.addNode("PERSON", "agata");
|
||||
|
||||
// the artifacts
|
||||
const graphviz = instance.addNode("ARTIFACT", "graph visualizer");
|
||||
const backend = instance.addNode("ARTIFACT", "backend");
|
||||
const frontend = instance.addNode("ARTIFACT", "frontend");
|
||||
const seededPagerank = instance.addNode("ARTIFACT", "seeded pagerank");
|
||||
const canvas = instance.addNode(
|
||||
"ARTIFACT",
|
||||
"the awesome illustrated poster board"
|
||||
);
|
||||
const logo = instance.addNode("ARTIFACT", "the broken-lightbulb logo");
|
||||
|
||||
instance.addEdge("DEPENDS_ON", prototype, graphviz);
|
||||
instance.addEdge("DEPENDS_ON", prototype, backend);
|
||||
instance.addEdge("DEPENDS_ON", prototype, frontend);
|
||||
instance.addEdge("DEPENDS_ON", frontend, seededPagerank);
|
||||
instance.addEdge("DEPENDS_ON", design, graphviz);
|
||||
instance.addEdge("DEPENDS_ON", design, frontend);
|
||||
instance.addEdge("DEPENDS_ON", design, logo);
|
||||
instance.addEdge("DEPENDS_ON", narrative, logo);
|
||||
instance.addEdge("DEPENDS_ON", narrative, canvas);
|
||||
|
||||
function addContribution(
|
||||
description: string,
|
||||
authors: Node[],
|
||||
impacted: Node[]
|
||||
) {
|
||||
const contrib = instance.addNode("CONTRIBUTION", description);
|
||||
for (const author of authors) {
|
||||
instance.addEdge("DEPENDS_ON", contrib, author);
|
||||
}
|
||||
for (const impact of impacted) {
|
||||
instance.addEdge("DEPENDS_ON", impact, contrib);
|
||||
}
|
||||
}
|
||||
|
||||
addContribution(
|
||||
"colors for the graph visualizer",
|
||||
[dennis, irene, lb, max, dl],
|
||||
[design, graphviz]
|
||||
);
|
||||
addContribution("design for graph visualizer", [dennis], [graphviz, design]);
|
||||
addContribution(
|
||||
"design for the frontend",
|
||||
[dennis, irene],
|
||||
[design, frontend]
|
||||
);
|
||||
addContribution(
|
||||
"pre-hack planning and project management",
|
||||
[brian],
|
||||
[prototype, logistics]
|
||||
);
|
||||
addContribution("implementing the graph visualizer", [dl], [graphviz]);
|
||||
addContribution("implementing seeded PageRank", [dl, mz], [seededPagerank]);
|
||||
addContribution("implementing the frontend", [dl, jmnemo], [frontend]);
|
||||
addContribution("implementing the backend", [dl], [backend]);
|
||||
addContribution("logo--preliminary work", [lb, agata], [logo]);
|
||||
addContribution("logo--lightbulb moment", [lb, max], [logo]);
|
||||
addContribution("drawing the canvas", [lb], [canvas]);
|
||||
addContribution(
|
||||
"narrative shaping for canvas",
|
||||
[lb, dl, mz],
|
||||
[canvas, narrative]
|
||||
);
|
||||
addContribution("oneline narrative statement", [dl, talbott], [narrative]);
|
||||
addContribution("oneline narrative review", [max, dl], [narrative]);
|
||||
addContribution("booking hotel stay", [sarah], [logistics]);
|
||||
addContribution("booking plane tickets", [sarah], [logistics]);
|
||||
addContribution("helping Dennis get into the space", [sarah], [logistics]);
|
||||
addContribution(
|
||||
"example prototype dataset",
|
||||
[mz, brian],
|
||||
[prototype, outreach]
|
||||
);
|
||||
addContribution("final presentation", [dl, prototype], [narrative]);
|
||||
addContribution("connection to the common stack", [mz], [outreach]);
|
||||
addContribution("discussing the canvas with folks", [lb], [outreach]);
|
||||
addContribution("general logistical defense", [jonathan], [logistics]);
|
||||
addContribution("recruiting Max & company", [mz], [outreach, logistics]);
|
||||
addContribution("forming the team", [mz], [logistics]);
|
||||
|
||||
return instance;
|
||||
}
|
|
@ -0,0 +1,48 @@
|
|||
// @flow
|
||||
|
||||
import type {Assets} from "../../webutil/assets";
|
||||
import type {PluginDeclaration} from "../../analysis/pluginDeclaration";
|
||||
import type {RepoId} from "../../core/repoId";
|
||||
import type {
|
||||
StaticExplorerAdapter as IStaticExplorerAdapter,
|
||||
DynamicExplorerAdapter as IDynamicExplorerAdapter,
|
||||
} from "../../explorer/adapters/explorerAdapter";
|
||||
import {NodeAddress, type NodeAddressT} from "../../core/graph";
|
||||
import {declaration} from "./declaration";
|
||||
import {OdysseyInstance} from "./instance";
|
||||
import {hackathonExample} from "./example";
|
||||
|
||||
export class StaticExplorerAdapter implements IStaticExplorerAdapter {
|
||||
declaration(): PluginDeclaration {
|
||||
return declaration;
|
||||
}
|
||||
|
||||
// TODO(@decentralion): Enable loading instances other than the hackathon example.
|
||||
async load(
|
||||
_unused_assets: Assets,
|
||||
_unused_repoId: RepoId
|
||||
): Promise<IDynamicExplorerAdapter> {
|
||||
const instance = hackathonExample();
|
||||
return new DynamicExplorerAdapter(instance);
|
||||
}
|
||||
}
|
||||
|
||||
class DynamicExplorerAdapter implements IDynamicExplorerAdapter {
|
||||
+_instance: OdysseyInstance;
|
||||
constructor(instance: OdysseyInstance): void {
|
||||
this._instance = instance;
|
||||
}
|
||||
nodeDescription(address: NodeAddressT) {
|
||||
const node = this._instance.node(address);
|
||||
if (node == null) {
|
||||
throw new Error(`No Odyssey node for: ${NodeAddress.toString(address)}`);
|
||||
}
|
||||
return node.description;
|
||||
}
|
||||
graph() {
|
||||
return this._instance.graph();
|
||||
}
|
||||
static() {
|
||||
return new StaticExplorerAdapter();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,225 @@
|
|||
// @flow
|
||||
|
||||
/**
|
||||
* Core "model" logic for the Odyssey plugin.
|
||||
* Basically allows creating a data store of priorities, contributions, and people,
|
||||
* and compiling that data store into a cred Graph.
|
||||
*/
|
||||
import {
|
||||
Graph,
|
||||
EdgeAddress,
|
||||
NodeAddress,
|
||||
type NodeAddressT,
|
||||
type GraphJSON,
|
||||
sortedNodeAddressesFromJSON,
|
||||
} from "../../core/graph";
|
||||
|
||||
import deepEqual from "lodash.isequal";
|
||||
|
||||
import {
|
||||
NODE_PREFIX,
|
||||
EDGE_PREFIX,
|
||||
type OdysseyNodeTypeIdentifier,
|
||||
isOdysseyNodeTypeIdentifier,
|
||||
type OdysseyEdgeTypeIdentifier,
|
||||
isOdysseyEdgeTypeIdentifier,
|
||||
} from "./declaration";
|
||||
|
||||
import {toCompat, fromCompat, type Compatible} from "../../util/compat";
|
||||
|
||||
import * as NullUtil from "../../util/null";
|
||||
|
||||
export type Node = {|
|
||||
+nodeTypeIdentifier: OdysseyNodeTypeIdentifier,
|
||||
+address: NodeAddressT,
|
||||
+description: string,
|
||||
|};
|
||||
|
||||
const COMPAT_INFO = {type: "sourcecred/odyssey/instance", version: "0.1.0"};
|
||||
export type InstanceJSON = Compatible<{|
|
||||
+graphJSON: GraphJSON,
|
||||
+sortedDescriptions: $ReadOnlyArray<string>,
|
||||
+count: number,
|
||||
|}>;
|
||||
|
||||
/**
|
||||
* This is the data model for a particular instance in the Odyssey Plugin.
|
||||
* The OdysseyInstance allows adding "Entities", which are basically nodes
|
||||
* in the Odyssey graph augmented with a type identifier, and a description.
|
||||
* Currently, the types are restricted to types hard-coded in the
|
||||
* [declaration](./declaration.js) but we intend to allow instance-specified
|
||||
* types in the future.
|
||||
*
|
||||
* The OdysseyInstance maintains an internal graph which actually stores the
|
||||
* node identities, as well as added nodes. You can get a copy of this graph
|
||||
* by calling the `.graph()` method.
|
||||
*
|
||||
* Entities are identified by an incrementing id (`._count`). This is
|
||||
* convenient for implementation, although it will make reconciling
|
||||
* simultaneous edits challenging. Once that becomes a real issue, we should
|
||||
* switch to a different node/edge identification strategy.
|
||||
*/
|
||||
export class OdysseyInstance {
|
||||
_graph: Graph;
|
||||
_descriptions: Map<NodeAddressT, string>;
|
||||
_count: number;
|
||||
|
||||
/**
|
||||
* Construct an Odyssey Instance.
|
||||
*/
|
||||
constructor() {
|
||||
this._graph = new Graph();
|
||||
this._descriptions = new Map();
|
||||
this._count = 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a new node to the instance (and a corresponding node to the graph).
|
||||
*
|
||||
* Requires a valid node type identifier (a string that uniquely identifies
|
||||
* an Odyssey node type; see [declaration.js](./declaration.js)).
|
||||
*/
|
||||
addNode(
|
||||
typeIdentifier: OdysseyNodeTypeIdentifier,
|
||||
description: string
|
||||
): Node {
|
||||
if (!isOdysseyNodeTypeIdentifier(typeIdentifier)) {
|
||||
throw new Error(
|
||||
`Tried to add node with invalid type identifier: ${typeIdentifier}`
|
||||
);
|
||||
}
|
||||
const address = NodeAddress.append(
|
||||
NODE_PREFIX,
|
||||
typeIdentifier,
|
||||
String(this._count)
|
||||
);
|
||||
this._graph.addNode(address);
|
||||
this._count++;
|
||||
this._descriptions.set(address, description);
|
||||
return NullUtil.get(this.node(address));
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the Node corresponding to a given node address, if it exists.
|
||||
*/
|
||||
node(address: NodeAddressT): ?Node {
|
||||
if (!this._graph.hasNode(address)) {
|
||||
return null;
|
||||
}
|
||||
const parts = NodeAddress.toParts(address);
|
||||
// We know it is an OdysseyNodeTypeIdentifier because the instance's internal
|
||||
// graph only has Odyssey nodes in it.
|
||||
const nodeTypeIdentifier: OdysseyNodeTypeIdentifier = (parts[2]: any);
|
||||
if (!isOdysseyNodeTypeIdentifier(nodeTypeIdentifier)) {
|
||||
throw new Error(
|
||||
`Invariant violation: ${nodeTypeIdentifier} is not odyssey type identifier`
|
||||
);
|
||||
}
|
||||
const description = NullUtil.get(this._descriptions.get(address));
|
||||
return {address, nodeTypeIdentifier, description};
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve all the Nodes in the instance.
|
||||
*
|
||||
* Optionally filter to only nodes of a chosen type.
|
||||
*/
|
||||
nodes(typeIdentifier?: OdysseyNodeTypeIdentifier): Iterator<Node> {
|
||||
const prefix =
|
||||
typeIdentifier == null
|
||||
? NODE_PREFIX
|
||||
: NodeAddress.append(NODE_PREFIX, typeIdentifier);
|
||||
return this._nodesIterator(prefix);
|
||||
}
|
||||
|
||||
*_nodesIterator(prefix: NodeAddressT): Iterator<Node> {
|
||||
for (const a of this._graph.nodes({prefix})) {
|
||||
yield NullUtil.get(this.node(a));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Add an edge to the Odyssey instance.
|
||||
*/
|
||||
// TODO(@decentralion): Add support for edge types (also configured on a per-instance basis).
|
||||
addEdge(
|
||||
type: OdysseyEdgeTypeIdentifier,
|
||||
src: Node,
|
||||
dst: Node
|
||||
): OdysseyInstance {
|
||||
if (!isOdysseyEdgeTypeIdentifier(type)) {
|
||||
throw new Error(`Invalid Odyssey edge type identifier: ${type}`);
|
||||
}
|
||||
const edge = {
|
||||
src: src.address,
|
||||
dst: dst.address,
|
||||
address: EdgeAddress.append(EDGE_PREFIX, type, String(this._count)),
|
||||
};
|
||||
this._graph.addEdge(edge);
|
||||
this._count++;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a copy of the graph underlying this instance.
|
||||
*/
|
||||
graph(): Graph {
|
||||
return this._graph.copy();
|
||||
}
|
||||
|
||||
toJSON(): InstanceJSON {
|
||||
const graphJSON = this._graph.toJSON();
|
||||
const sortedNodeAddresses = sortedNodeAddressesFromJSON(graphJSON);
|
||||
const sortedDescriptions = sortedNodeAddresses.map((a) =>
|
||||
NullUtil.get(this._descriptions.get(a))
|
||||
);
|
||||
return toCompat(COMPAT_INFO, {
|
||||
graphJSON,
|
||||
sortedDescriptions,
|
||||
count: this._count,
|
||||
});
|
||||
}
|
||||
|
||||
static fromJSON(j: InstanceJSON): OdysseyInstance {
|
||||
const {graphJSON, sortedDescriptions, count} = fromCompat(COMPAT_INFO, j);
|
||||
const instance = new OdysseyInstance();
|
||||
instance._graph = Graph.fromJSON(graphJSON);
|
||||
instance._count = count;
|
||||
const descriptions = new Map();
|
||||
const sortedNodeAddresses = sortedNodeAddressesFromJSON(graphJSON);
|
||||
for (let i = 0; i < sortedNodeAddresses.length; i++) {
|
||||
descriptions.set(sortedNodeAddresses[i], sortedDescriptions[i]);
|
||||
}
|
||||
instance._descriptions = descriptions;
|
||||
return instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether two Odyssey instances have identical histories.
|
||||
*
|
||||
* Two instances are historically identical if they have the same ordered
|
||||
* sequence of node additions and deletions. This is because the address of
|
||||
* Nodes and Edges in the instance is determined by the order in which they
|
||||
* were added.
|
||||
*
|
||||
* For an illustration, consider the following case:
|
||||
* ```js
|
||||
* const i1 = new OdysseyInstance();
|
||||
* i1.addNode("PERSON", "me")
|
||||
* i1.addNode("PERSON", "you")
|
||||
*
|
||||
* const i2 = new OdysseyInstance();
|
||||
* i2.addNode("PERSON", "you")
|
||||
* i2.addNode("PERSON", "me")
|
||||
*
|
||||
* expect(i1.isHistoricallyIdentical(i2)).toBe(false);
|
||||
* ```
|
||||
*/
|
||||
isHistoricallyIdentical(that: OdysseyInstance): boolean {
|
||||
return (
|
||||
this._count === that._count &&
|
||||
this._graph.equals(that._graph) &&
|
||||
deepEqual(this._descriptions, that._descriptions)
|
||||
);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,152 @@
|
|||
// @flow
|
||||
|
||||
import {OdysseyInstance} from "./instance";
|
||||
import {EdgeAddress, NodeAddress, Direction} from "../../core/graph";
|
||||
|
||||
describe("plugins/odyssey/instance", () => {
|
||||
function exampleInstance() {
|
||||
const instance = new OdysseyInstance();
|
||||
const me = instance.addNode("PERSON", "me");
|
||||
const you = instance.addNode("PERSON", "you");
|
||||
const value = instance.addNode("VALUE", "valuable-ness");
|
||||
const contribution = instance.addNode("CONTRIBUTION", "a good deed");
|
||||
const artifact = instance.addNode(
|
||||
"ARTIFACT",
|
||||
"the thing that creates value"
|
||||
);
|
||||
|
||||
// first off I'd like to thank myself
|
||||
instance
|
||||
.addEdge("DEPENDS_ON", me, me)
|
||||
// thank you for your support
|
||||
.addEdge("DEPENDS_ON", me, you)
|
||||
.addEdge("DEPENDS_ON", contribution, me)
|
||||
.addEdge("DEPENDS_ON", artifact, contribution)
|
||||
.addEdge("DEPENDS_ON", value, artifact);
|
||||
return {instance, you, me, value, contribution, artifact};
|
||||
}
|
||||
it("can retrieve the graph", () => {
|
||||
const {instance, me} = exampleInstance();
|
||||
const graph = instance.graph();
|
||||
const nodes = Array.from(graph.nodes());
|
||||
const edges = Array.from(graph.edges());
|
||||
expect(nodes).toHaveLength(5);
|
||||
expect(edges).toHaveLength(5);
|
||||
const myNeighbors = graph.neighbors(me.address, {
|
||||
direction: Direction.ANY,
|
||||
nodePrefix: NodeAddress.empty,
|
||||
edgePrefix: EdgeAddress.empty,
|
||||
});
|
||||
expect(Array.from(myNeighbors)).toHaveLength(3);
|
||||
});
|
||||
it("retrieved graph is a copy", () => {
|
||||
const {instance} = exampleInstance();
|
||||
expect(instance.graph()).not.toBe(instance.graph());
|
||||
});
|
||||
it("returns nodes as they are added", () => {
|
||||
const {me} = exampleInstance();
|
||||
expect(me).toEqual({
|
||||
address: NodeAddress.fromParts(["sourcecred", "odyssey", "PERSON", "0"]),
|
||||
nodeTypeIdentifier: "PERSON",
|
||||
description: "me",
|
||||
});
|
||||
});
|
||||
it("can retrieve nodes by address", () => {
|
||||
const {instance, me} = exampleInstance();
|
||||
expect(instance.node(me.address)).toEqual(me);
|
||||
});
|
||||
it("returns null for non-existent node", () => {
|
||||
const instance = new OdysseyInstance();
|
||||
expect(instance.node(NodeAddress.empty)).toEqual(null);
|
||||
});
|
||||
it("throws an error when adding an node with bad type", () => {
|
||||
const instance = new OdysseyInstance();
|
||||
// $ExpectFlowError
|
||||
expect(() => instance.addNode("FOO", "foo")).toThrowError(
|
||||
"invalid type identifier: FOO"
|
||||
);
|
||||
});
|
||||
it("can retrieve all nodes", () => {
|
||||
const {instance} = exampleInstance();
|
||||
expect(Array.from(instance.nodes())).toHaveLength(5);
|
||||
});
|
||||
it("can retrieve nodes by type", () => {
|
||||
const {instance, artifact, contribution, value} = exampleInstance();
|
||||
expect(Array.from(instance.nodes("ARTIFACT"))).toEqual([artifact]);
|
||||
expect(Array.from(instance.nodes("VALUE"))).toEqual([value]);
|
||||
expect(Array.from(instance.nodes("CONTRIBUTION"))).toEqual([contribution]);
|
||||
// $ExpectFlowError
|
||||
expect(Array.from(instance.nodes("NONEXISTENT"))).toEqual([]);
|
||||
});
|
||||
it("errors if adding edge between Nodes that don't exist", () => {
|
||||
const {me, you} = exampleInstance();
|
||||
const i = new OdysseyInstance();
|
||||
expect(() => i.addEdge("DEPENDS_ON", me, you)).toThrowError(
|
||||
"Missing src on edge:"
|
||||
);
|
||||
});
|
||||
describe("equality", () => {
|
||||
it("empty instance isHistoricallyIdentical empty instance", () => {
|
||||
const a = new OdysseyInstance();
|
||||
const b = new OdysseyInstance();
|
||||
expect(a.isHistoricallyIdentical(b)).toBe(true);
|
||||
});
|
||||
it("empty instance does not equal nonempty instance", () => {
|
||||
const a = new OdysseyInstance();
|
||||
a.addNode("PERSON", "me");
|
||||
const b = new OdysseyInstance();
|
||||
expect(a.isHistoricallyIdentical(b)).toBe(false);
|
||||
});
|
||||
it("complex but identically generated instances are equal", () => {
|
||||
const {instance: a} = exampleInstance();
|
||||
const {instance: b} = exampleInstance();
|
||||
expect(a.isHistoricallyIdentical(b)).toBe(true);
|
||||
});
|
||||
it("instances with different descriptions are not equal", () => {
|
||||
const a = new OdysseyInstance();
|
||||
const b = new OdysseyInstance();
|
||||
a.addNode("PERSON", "me");
|
||||
b.addNode("PERSON", "me2");
|
||||
expect(a.isHistoricallyIdentical(b)).toBe(false);
|
||||
});
|
||||
it("instances with different types are not equal", () => {
|
||||
const a = new OdysseyInstance();
|
||||
const b = new OdysseyInstance();
|
||||
a.addNode("PERSON", "me");
|
||||
b.addNode("ARTIFACT", "me");
|
||||
expect(a.isHistoricallyIdentical(b)).toBe(false);
|
||||
});
|
||||
it("instances with different edges are not equal", () => {
|
||||
const a = new OdysseyInstance();
|
||||
const b = new OdysseyInstance();
|
||||
const me = a.addNode("PERSON", "me");
|
||||
b.addNode("PERSON", "me");
|
||||
a.addEdge("DEPENDS_ON", me, me);
|
||||
expect(a.isHistoricallyIdentical(b)).toBe(false);
|
||||
});
|
||||
it("instances with different histories are not equal", () => {
|
||||
const i1 = new OdysseyInstance();
|
||||
i1.addNode("PERSON", "me");
|
||||
i1.addNode("PERSON", "you");
|
||||
|
||||
const i2 = new OdysseyInstance();
|
||||
i2.addNode("PERSON", "you");
|
||||
i2.addNode("PERSON", "me");
|
||||
|
||||
expect(i1.isHistoricallyIdentical(i2)).toBe(false);
|
||||
});
|
||||
});
|
||||
describe("to/from JSON", () => {
|
||||
it("to->fro is identity", () => {
|
||||
const {instance} = exampleInstance();
|
||||
const instance2 = OdysseyInstance.fromJSON(instance.toJSON());
|
||||
expect(instance.isHistoricallyIdentical(instance2)).toBe(true);
|
||||
});
|
||||
it("fro->to is identity", () => {
|
||||
const {instance} = exampleInstance();
|
||||
const json1 = instance.toJSON();
|
||||
const json2 = OdysseyInstance.fromJSON(json1).toJSON();
|
||||
expect(json1).toEqual(json2);
|
||||
});
|
||||
});
|
||||
});
|
Loading…
Reference in New Issue