whygraph 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (92) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +205 -0
  3. package/dist/cli/commands/config.d.ts +14 -0
  4. package/dist/cli/commands/config.js +123 -0
  5. package/dist/cli/commands/down.d.ts +9 -0
  6. package/dist/cli/commands/down.js +46 -0
  7. package/dist/cli/commands/init.d.ts +17 -0
  8. package/dist/cli/commands/init.js +144 -0
  9. package/dist/cli/commands/issues.d.ts +10 -0
  10. package/dist/cli/commands/issues.js +376 -0
  11. package/dist/cli/commands/mcp.d.ts +2 -0
  12. package/dist/cli/commands/mcp.js +9 -0
  13. package/dist/cli/commands/restart.d.ts +11 -0
  14. package/dist/cli/commands/restart.js +43 -0
  15. package/dist/cli/commands/serve.d.ts +14 -0
  16. package/dist/cli/commands/serve.js +132 -0
  17. package/dist/cli/commands/server-utils.d.ts +6 -0
  18. package/dist/cli/commands/server-utils.js +94 -0
  19. package/dist/cli/commands/status.d.ts +11 -0
  20. package/dist/cli/commands/status.js +97 -0
  21. package/dist/cli/commands/up.d.ts +13 -0
  22. package/dist/cli/commands/up.js +62 -0
  23. package/dist/cli/commands/validate.d.ts +14 -0
  24. package/dist/cli/commands/validate.js +88 -0
  25. package/dist/cli/commands/viz.d.ts +7 -0
  26. package/dist/cli/commands/viz.js +97 -0
  27. package/dist/cli/index.d.ts +2 -0
  28. package/dist/cli/index.js +33 -0
  29. package/dist/entity/id.d.ts +8 -0
  30. package/dist/entity/id.js +48 -0
  31. package/dist/entity/issues.d.ts +12 -0
  32. package/dist/entity/issues.js +68 -0
  33. package/dist/entity/parser.d.ts +6 -0
  34. package/dist/entity/parser.js +166 -0
  35. package/dist/entity/types.d.ts +54 -0
  36. package/dist/entity/types.js +21 -0
  37. package/dist/entity/validate.d.ts +12 -0
  38. package/dist/entity/validate.js +136 -0
  39. package/dist/entity/writer.d.ts +16 -0
  40. package/dist/entity/writer.js +142 -0
  41. package/dist/frontend/assets/index-ByZzPwVe.css +1 -0
  42. package/dist/frontend/assets/index-F9dxfzD_.js +170 -0
  43. package/dist/frontend/index.html +14 -0
  44. package/dist/graph/cascade.d.ts +10 -0
  45. package/dist/graph/cascade.js +49 -0
  46. package/dist/graph/decisions.d.ts +11 -0
  47. package/dist/graph/decisions.js +27 -0
  48. package/dist/graph/gaps.d.ts +10 -0
  49. package/dist/graph/gaps.js +58 -0
  50. package/dist/graph/nodes.d.ts +20 -0
  51. package/dist/graph/nodes.js +33 -0
  52. package/dist/graph/projection.d.ts +6 -0
  53. package/dist/graph/projection.js +44 -0
  54. package/dist/graph/query.d.ts +15 -0
  55. package/dist/graph/query.js +82 -0
  56. package/dist/graph/search.d.ts +2 -0
  57. package/dist/graph/search.js +23 -0
  58. package/dist/graph/supersede.d.ts +7 -0
  59. package/dist/graph/supersede.js +48 -0
  60. package/dist/graph/temporal.d.ts +13 -0
  61. package/dist/graph/temporal.js +28 -0
  62. package/dist/mcp/index.d.ts +2 -0
  63. package/dist/mcp/index.js +10 -0
  64. package/dist/mcp/server.d.ts +3 -0
  65. package/dist/mcp/server.js +340 -0
  66. package/dist/onboarding/interview.d.ts +22 -0
  67. package/dist/onboarding/interview.js +92 -0
  68. package/dist/onboarding/scan.d.ts +17 -0
  69. package/dist/onboarding/scan.js +106 -0
  70. package/dist/platform/rules.d.ts +8 -0
  71. package/dist/platform/rules.js +229 -0
  72. package/dist/server/core.d.ts +26 -0
  73. package/dist/server/core.js +111 -0
  74. package/dist/server/derived.d.ts +8 -0
  75. package/dist/server/derived.js +13 -0
  76. package/dist/server/etag.d.ts +9 -0
  77. package/dist/server/etag.js +25 -0
  78. package/dist/server/http.d.ts +13 -0
  79. package/dist/server/http.js +131 -0
  80. package/dist/server/pubsub.d.ts +12 -0
  81. package/dist/server/pubsub.js +19 -0
  82. package/dist/server/schema.d.ts +2 -0
  83. package/dist/server/schema.js +362 -0
  84. package/dist/server/stale-refs.d.ts +7 -0
  85. package/dist/server/stale-refs.js +23 -0
  86. package/dist/server/watcher.d.ts +21 -0
  87. package/dist/server/watcher.js +98 -0
  88. package/dist/server/worktree-watcher.d.ts +20 -0
  89. package/dist/server/worktree-watcher.js +79 -0
  90. package/dist/server/worktree.d.ts +22 -0
  91. package/dist/server/worktree.js +84 -0
  92. package/package.json +73 -0
@@ -0,0 +1,14 @@
1
+ <!doctype html>
2
+ <html lang="en" data-theme="dark">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <meta name="color-scheme" content="dark light" />
7
+ <title>whygraph</title>
8
+ <script type="module" crossorigin src="/assets/index-F9dxfzD_.js"></script>
9
+ <link rel="stylesheet" crossorigin href="/assets/index-ByZzPwVe.css">
10
+ </head>
11
+ <body>
12
+ <div id="root"></div>
13
+ </body>
14
+ </html>
@@ -0,0 +1,10 @@
1
+ import type { Entity } from "../entity/types.js";
2
+ export interface CascadeResult {
3
+ removedIds: string[];
4
+ orphanedDecisionIds: string[];
5
+ partialDecisionPatches: Array<{
6
+ id: string;
7
+ remainingAffects: string[];
8
+ }>;
9
+ }
10
+ export declare function cascadeRemoval(entities: Map<string, Entity>, nodeId: string, timestamp: string): CascadeResult;
@@ -0,0 +1,49 @@
1
+ import graphology from "graphology";
2
+ import { buildGraph } from "./projection.js";
3
+ import { isDecisionNode } from "../entity/types.js";
4
+ const MultiDirectedGraph = graphology.MultiDirectedGraph;
5
+ export function cascadeRemoval(entities, nodeId, timestamp) {
6
+ const entityArray = Array.from(entities.values());
7
+ const graph = buildGraph(entityArray);
8
+ // Collect all descendants via COMPOSES edges (recursive)
9
+ const removalSet = new Set();
10
+ const queue = [nodeId];
11
+ while (queue.length > 0) {
12
+ const current = queue.pop();
13
+ /* v8 ignore next 1 */
14
+ if (removalSet.has(current))
15
+ continue;
16
+ if (!graph.hasNode(current))
17
+ continue;
18
+ removalSet.add(current);
19
+ // Find COMPOSES children: edges where current is source with label COMPOSES
20
+ graph.forEachOutEdge(current, (_edge, attrs, _source, target) => {
21
+ if (attrs.label === "COMPOSES" && !removalSet.has(target)) {
22
+ queue.push(target);
23
+ }
24
+ });
25
+ }
26
+ const removedIds = Array.from(removalSet);
27
+ const orphanedDecisionIds = [];
28
+ const partialDecisionPatches = [];
29
+ // Check all decisions
30
+ for (const entity of entityArray) {
31
+ if (!isDecisionNode(entity))
32
+ continue;
33
+ if (entity.removed_at !== undefined)
34
+ continue;
35
+ if (removalSet.has(entity.id))
36
+ continue;
37
+ const affectsInRemoval = entity.affects.filter((id) => removalSet.has(id));
38
+ if (affectsInRemoval.length === 0)
39
+ continue;
40
+ const remaining = entity.affects.filter((id) => !removalSet.has(id));
41
+ if (remaining.length === 0) {
42
+ orphanedDecisionIds.push(entity.id);
43
+ }
44
+ else {
45
+ partialDecisionPatches.push({ id: entity.id, remainingAffects: remaining });
46
+ }
47
+ }
48
+ return { removedIds, orphanedDecisionIds, partialDecisionPatches };
49
+ }
@@ -0,0 +1,11 @@
1
+ import graphology from "graphology";
2
+ import type { DecisionNode, DecisionStatus, DecisionTag } from "../entity/types.js";
3
+ type MultiDirectedGraph = graphology.MultiDirectedGraph;
4
+ export interface DecisionFilters {
5
+ status?: DecisionStatus;
6
+ tags?: DecisionTag[];
7
+ dateFrom?: string;
8
+ dateTo?: string;
9
+ }
10
+ export declare function getDecisions(graph: MultiDirectedGraph, filters?: DecisionFilters): DecisionNode[];
11
+ export {};
@@ -0,0 +1,27 @@
1
+ export function getDecisions(graph, filters) {
2
+ const results = [];
3
+ graph.forEachNode((nodeId, attributes) => {
4
+ if (attributes.label !== "Decision")
5
+ return;
6
+ // Exclude removed decisions
7
+ if (attributes.removed_at !== undefined)
8
+ return;
9
+ // Status filter (AND)
10
+ if (filters?.status !== undefined && attributes.status !== filters.status)
11
+ return;
12
+ // Tags filter (OR within tags)
13
+ if (filters?.tags !== undefined && filters.tags.length > 0) {
14
+ const nodeTags = attributes.tags;
15
+ const hasMatch = filters.tags.some((tag) => nodeTags.includes(tag));
16
+ if (!hasMatch)
17
+ return;
18
+ }
19
+ // Date range filters (AND)
20
+ if (filters?.dateFrom !== undefined && attributes.date < filters.dateFrom)
21
+ return;
22
+ if (filters?.dateTo !== undefined && attributes.date > filters.dateTo)
23
+ return;
24
+ results.push({ id: nodeId, ...attributes });
25
+ });
26
+ return results;
27
+ }
@@ -0,0 +1,10 @@
1
+ import graphology from "graphology";
2
+ import type { StructuralNode } from "../entity/types.js";
3
+ type MultiDirectedGraph = graphology.MultiDirectedGraph;
4
+ /**
5
+ * Find Feature/Component nodes with no inbound AFFECTS edges from any Decision.
6
+ * Order: Features first, then Components by COMPOSES depth (shallow first).
7
+ * Excludes removed nodes and App nodes.
8
+ */
9
+ export declare function getGaps(graph: MultiDirectedGraph, limit?: number): StructuralNode[];
10
+ export {};
@@ -0,0 +1,58 @@
1
+ /**
2
+ * Find Feature/Component nodes with no inbound AFFECTS edges from any Decision.
3
+ * Order: Features first, then Components by COMPOSES depth (shallow first).
4
+ * Excludes removed nodes and App nodes.
5
+ */
6
+ export function getGaps(graph, limit) {
7
+ const gaps = [];
8
+ graph.forEachNode((nodeId, attributes) => {
9
+ // Skip removed nodes
10
+ if (attributes.removed_at)
11
+ return;
12
+ // Only Feature and Component nodes
13
+ const label = attributes.label;
14
+ if (label !== "Feature" && label !== "Component")
15
+ return;
16
+ // Check for inbound AFFECTS edges
17
+ const hasAffects = graph.someInEdge(nodeId, (_edge, edgeAttrs) => {
18
+ return edgeAttrs.label === "AFFECTS";
19
+ });
20
+ if (!hasAffects) {
21
+ gaps.push({ id: nodeId, ...attributes });
22
+ }
23
+ });
24
+ // Sort: Features first, then Components by COMPOSES depth (shallow first)
25
+ gaps.sort((a, b) => {
26
+ if (a.label !== b.label) {
27
+ return a.label === "Feature" ? -1 : 1;
28
+ }
29
+ if (a.label === "Component" && b.label === "Component") {
30
+ const depthA = getComposesDepth(graph, a.id);
31
+ const depthB = getComposesDepth(graph, b.id);
32
+ return depthA - depthB;
33
+ }
34
+ return 0;
35
+ });
36
+ if (limit !== undefined) {
37
+ return gaps.slice(0, limit);
38
+ }
39
+ return gaps;
40
+ }
41
+ /** Count how many COMPOSES edges exist from root to this node. */
42
+ function getComposesDepth(graph, nodeId) {
43
+ let depth = 0;
44
+ let current = nodeId;
45
+ while (true) {
46
+ let parentFound = null;
47
+ graph.forEachInEdge(current, (_edge, edgeAttrs, source) => {
48
+ if (edgeAttrs.label === "COMPOSES") {
49
+ parentFound = source;
50
+ }
51
+ });
52
+ if (parentFound === null)
53
+ break;
54
+ depth++;
55
+ current = parentFound;
56
+ }
57
+ return depth;
58
+ }
@@ -0,0 +1,20 @@
1
+ import graphology from "graphology";
2
+ import type { NodeLabel } from "../entity/types.js";
3
+ type MultiDirectedGraph = graphology.MultiDirectedGraph;
4
+ export interface NodeFilters {
5
+ label?: NodeLabel;
6
+ parent?: string;
7
+ search?: string;
8
+ }
9
+ export interface NodeSummary {
10
+ id: string;
11
+ label: NodeLabel;
12
+ name: string;
13
+ parent?: string;
14
+ }
15
+ /**
16
+ * List nodes with optional filtering. Returns lightweight summaries.
17
+ * Excludes removed nodes.
18
+ */
19
+ export declare function listNodes(graph: MultiDirectedGraph, filters?: NodeFilters): NodeSummary[];
20
+ export {};
@@ -0,0 +1,33 @@
1
+ /**
2
+ * List nodes with optional filtering. Returns lightweight summaries.
3
+ * Excludes removed nodes.
4
+ */
5
+ export function listNodes(graph, filters) {
6
+ const results = [];
7
+ graph.forEachNode((nodeId, attributes) => {
8
+ // Exclude removed nodes
9
+ if (attributes.removed_at)
10
+ return;
11
+ const label = attributes.label;
12
+ const name = (attributes.name ?? attributes.title);
13
+ const parent = attributes.parent;
14
+ // Filter by label
15
+ if (filters?.label && label !== filters.label)
16
+ return;
17
+ // Filter by parent
18
+ if (filters?.parent && parent !== filters.parent)
19
+ return;
20
+ // Filter by search (case-insensitive substring on name/title)
21
+ if (filters?.search) {
22
+ const searchLower = filters.search.toLowerCase();
23
+ if (!name.toLowerCase().includes(searchLower))
24
+ return;
25
+ }
26
+ const summary = { id: nodeId, label, name };
27
+ if (parent !== undefined) {
28
+ summary.parent = parent;
29
+ }
30
+ results.push(summary);
31
+ });
32
+ return results;
33
+ }
@@ -0,0 +1,6 @@
1
+ import graphology from "graphology";
2
+ import type { Entity } from "../entity/types.js";
3
+ type MultiDirectedGraph = graphology.MultiDirectedGraph;
4
+ declare const MultiDirectedGraph: typeof graphology.MultiDirectedGraph;
5
+ export declare function buildGraph(entities: Entity[]): MultiDirectedGraph;
6
+ export {};
@@ -0,0 +1,44 @@
1
+ import graphology from "graphology";
2
+ import { isStructuralNode, isDecisionNode } from "../entity/types.js";
3
+ const MultiDirectedGraph = graphology.MultiDirectedGraph;
4
+ export function buildGraph(entities) {
5
+ const graph = new MultiDirectedGraph();
6
+ // Phase 1: add all nodes
7
+ for (const entity of entities) {
8
+ const { id, ...attributes } = entity;
9
+ graph.addNode(id, attributes);
10
+ }
11
+ // Phase 2: derive edges
12
+ for (const entity of entities) {
13
+ // COMPOSES: parent → child
14
+ if (isStructuralNode(entity) && entity.parent !== undefined) {
15
+ if (graph.hasNode(entity.parent)) {
16
+ graph.addEdgeWithKey(`composes-${entity.parent}-${entity.id}`, entity.parent, entity.id, { label: "COMPOSES" });
17
+ }
18
+ else {
19
+ console.warn(`Skipping COMPOSES edge: parent "${entity.parent}" not found for node "${entity.id}"`);
20
+ }
21
+ }
22
+ // AFFECTS: decision → affected node
23
+ if (isDecisionNode(entity)) {
24
+ for (const targetId of entity.affects) {
25
+ if (graph.hasNode(targetId)) {
26
+ graph.addEdgeWithKey(`affects-${entity.id}-${targetId}`, entity.id, targetId, { label: "AFFECTS" });
27
+ }
28
+ else {
29
+ console.warn(`Skipping AFFECTS edge: target "${targetId}" not found for decision "${entity.id}"`);
30
+ }
31
+ }
32
+ // SUPERSEDES: new → old
33
+ if (entity.supersedes !== undefined) {
34
+ if (graph.hasNode(entity.supersedes)) {
35
+ graph.addEdgeWithKey(`supersedes-${entity.id}-${entity.supersedes}`, entity.id, entity.supersedes, { label: "SUPERSEDES" });
36
+ }
37
+ else {
38
+ console.warn(`Skipping SUPERSEDES edge: target "${entity.supersedes}" not found for decision "${entity.id}"`);
39
+ }
40
+ }
41
+ }
42
+ }
43
+ return graph;
44
+ }
@@ -0,0 +1,15 @@
1
+ import graphology from "graphology";
2
+ import type { DecisionNode } from "../entity/types.js";
3
+ type MultiDirectedGraph = graphology.MultiDirectedGraph;
4
+ export interface ContextNodeResult {
5
+ id: string;
6
+ label: string;
7
+ name: string;
8
+ parentChain: string[];
9
+ }
10
+ export interface ContextResult {
11
+ nodes: ContextNodeResult[];
12
+ decisions: DecisionNode[];
13
+ }
14
+ export declare function getContext(graph: MultiDirectedGraph, file: string, symbol?: string): ContextResult;
15
+ export {};
@@ -0,0 +1,82 @@
1
+ function refMatches(refs, file, symbol) {
2
+ for (const ref of refs) {
3
+ if (ref.file !== file)
4
+ continue;
5
+ // File-level ref (no symbol) always matches
6
+ if (ref.symbol === undefined)
7
+ return true;
8
+ // If no symbol filter requested, any file match counts
9
+ if (symbol === undefined)
10
+ return true;
11
+ // Symbol must match
12
+ if (ref.symbol === symbol)
13
+ return true;
14
+ }
15
+ return false;
16
+ }
17
+ function getParentChain(graph, nodeId) {
18
+ const chain = [];
19
+ let current = nodeId;
20
+ while (true) {
21
+ // Find incoming COMPOSES edges (parent -> current)
22
+ const inEdges = graph.inEdges(current);
23
+ let foundParent = false;
24
+ for (const edgeKey of inEdges) {
25
+ if (graph.getEdgeAttribute(edgeKey, "label") === "COMPOSES") {
26
+ const parent = graph.source(edgeKey);
27
+ chain.push(parent);
28
+ current = parent;
29
+ foundParent = true;
30
+ break;
31
+ }
32
+ }
33
+ if (!foundParent)
34
+ break;
35
+ }
36
+ return chain;
37
+ }
38
+ function collectDecisions(graph, nodeIds) {
39
+ const decisions = new Map();
40
+ for (const nodeId of nodeIds) {
41
+ // Find incoming AFFECTS edges (decision -> node)
42
+ const inEdges = graph.inEdges(nodeId);
43
+ for (const edgeKey of inEdges) {
44
+ if (graph.getEdgeAttribute(edgeKey, "label") === "AFFECTS") {
45
+ const decisionId = graph.source(edgeKey);
46
+ /* v8 ignore next 1 */
47
+ if (!decisions.has(decisionId)) {
48
+ decisions.set(decisionId, {
49
+ id: decisionId,
50
+ ...graph.getNodeAttributes(decisionId),
51
+ });
52
+ }
53
+ }
54
+ }
55
+ }
56
+ return Array.from(decisions.values());
57
+ }
58
+ export function getContext(graph, file, symbol) {
59
+ const matchedNodes = [];
60
+ const allRelevantNodeIds = new Set();
61
+ graph.forEachNode((nodeId, attributes) => {
62
+ const refs = attributes.refs;
63
+ if (!refs || refs.length === 0)
64
+ return;
65
+ if (!refMatches(refs, file, symbol))
66
+ return;
67
+ const parentChain = getParentChain(graph, nodeId);
68
+ matchedNodes.push({
69
+ id: nodeId,
70
+ label: attributes.label,
71
+ name: attributes.name,
72
+ parentChain,
73
+ });
74
+ // Collect this node and all ancestors for decision lookup
75
+ allRelevantNodeIds.add(nodeId);
76
+ for (const ancestorId of parentChain) {
77
+ allRelevantNodeIds.add(ancestorId);
78
+ }
79
+ });
80
+ const decisions = collectDecisions(graph, allRelevantNodeIds);
81
+ return { nodes: matchedNodes, decisions };
82
+ }
@@ -0,0 +1,2 @@
1
+ import type { Entity, DecisionNode } from "../entity/types.js";
2
+ export declare function searchDecisions(entities: Map<string, Entity>, query: string): DecisionNode[];
@@ -0,0 +1,23 @@
1
+ import { isDecisionNode } from "../entity/types.js";
2
+ export function searchDecisions(entities, query) {
3
+ const lowerQuery = query.toLowerCase();
4
+ const results = [];
5
+ for (const entity of entities.values()) {
6
+ if (!isDecisionNode(entity))
7
+ continue;
8
+ if (entity.removed_at !== undefined)
9
+ continue;
10
+ const searchable = [
11
+ entity.title,
12
+ entity.context,
13
+ entity.decision,
14
+ entity.tradeoffs,
15
+ entity.alternatives,
16
+ ];
17
+ const matches = searchable.some((field) => field.toLowerCase().includes(lowerQuery));
18
+ if (matches) {
19
+ results.push(entity);
20
+ }
21
+ }
22
+ return results;
23
+ }
@@ -0,0 +1,7 @@
1
+ import type { Entity } from "../entity/types.js";
2
+ export interface SupersedeCandidate {
3
+ newDecisionId: string;
4
+ existingDecisionId: string;
5
+ sharedNodeIds: string[];
6
+ }
7
+ export declare function detectSupersedeCandidates(entities: Map<string, Entity>): SupersedeCandidate[];
@@ -0,0 +1,48 @@
1
+ import { isDecisionNode } from "../entity/types.js";
2
+ export function detectSupersedeCandidates(entities) {
3
+ // Collect active, non-removed decisions
4
+ const activeDecisions = [];
5
+ for (const entity of entities.values()) {
6
+ if (!isDecisionNode(entity))
7
+ continue;
8
+ if (entity.removed_at !== undefined)
9
+ continue;
10
+ if (entity.status === "superseded")
11
+ continue;
12
+ activeDecisions.push(entity);
13
+ }
14
+ // Build a set of existing supersedes relationships for exclusion
15
+ const supersededPairs = new Set();
16
+ for (const entity of entities.values()) {
17
+ if (!isDecisionNode(entity))
18
+ continue;
19
+ if (entity.supersedes !== undefined) {
20
+ // Normalize as "newer->older"
21
+ supersededPairs.add(`${entity.id}->${entity.supersedes}`);
22
+ }
23
+ }
24
+ const candidates = [];
25
+ for (let i = 0; i < activeDecisions.length; i++) {
26
+ for (let j = i + 1; j < activeDecisions.length; j++) {
27
+ const a = activeDecisions[i];
28
+ const b = activeDecisions[j];
29
+ // Check if already in a supersedes relationship (either direction)
30
+ if (supersededPairs.has(`${a.id}->${b.id}`) ||
31
+ supersededPairs.has(`${b.id}->${a.id}`)) {
32
+ continue;
33
+ }
34
+ const aSet = new Set(a.affects);
35
+ const shared = b.affects.filter((id) => aSet.has(id));
36
+ if (shared.length > 0) {
37
+ // Newer decision (later date or later created_at) is "new", other is "existing"
38
+ const aIsNewer = a.created_at > b.created_at;
39
+ candidates.push({
40
+ newDecisionId: aIsNewer ? a.id : b.id,
41
+ existingDecisionId: aIsNewer ? b.id : a.id,
42
+ sharedNodeIds: shared,
43
+ });
44
+ }
45
+ }
46
+ }
47
+ return candidates;
48
+ }
@@ -0,0 +1,13 @@
1
+ import graphology from "graphology";
2
+ import type { Entity } from "../entity/types.js";
3
+ type MultiDirectedGraph = graphology.MultiDirectedGraph;
4
+ /**
5
+ * Build a graph projection at a specific point in time.
6
+ * Includes entities where created_at <= timestamp AND (removed_at is null OR removed_at > timestamp).
7
+ */
8
+ export declare function buildGraphAt(entities: Entity[], timestamp: string): MultiDirectedGraph;
9
+ /**
10
+ * Collect all unique timestamps (created_at and removed_at) from entities, sorted chronologically.
11
+ */
12
+ export declare function getTimestamps(entities: Entity[]): string[];
13
+ export {};
@@ -0,0 +1,28 @@
1
+ import { buildGraph } from "./projection.js";
2
+ /**
3
+ * Build a graph projection at a specific point in time.
4
+ * Includes entities where created_at <= timestamp AND (removed_at is null OR removed_at > timestamp).
5
+ */
6
+ export function buildGraphAt(entities, timestamp) {
7
+ const filtered = entities.filter((entity) => {
8
+ if (entity.created_at > timestamp)
9
+ return false;
10
+ if (entity.removed_at !== undefined && entity.removed_at <= timestamp)
11
+ return false;
12
+ return true;
13
+ });
14
+ return buildGraph(filtered);
15
+ }
16
+ /**
17
+ * Collect all unique timestamps (created_at and removed_at) from entities, sorted chronologically.
18
+ */
19
+ export function getTimestamps(entities) {
20
+ const set = new Set();
21
+ for (const entity of entities) {
22
+ set.add(entity.created_at);
23
+ if (entity.removed_at !== undefined) {
24
+ set.add(entity.removed_at);
25
+ }
26
+ }
27
+ return [...set].sort();
28
+ }
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
@@ -0,0 +1,10 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * MCP server entrypoint. When run directly, starts the stdio-based MCP server.
4
+ * For programmatic use, import from ./server.js instead.
5
+ */
6
+ import { startMcpServer } from "./server.js";
7
+ startMcpServer().catch((err) => {
8
+ console.error("Failed to start MCP server:", err);
9
+ process.exit(1);
10
+ });
@@ -0,0 +1,3 @@
1
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ export declare function createMcpServer(): McpServer;
3
+ export declare function startMcpServer(): Promise<void>;