Skip to content

Pathfinding Engine

Overview

The pathfinding engine finds the N shortest weighted paths between a source node and a target node in Pathorah's heterogeneous professional graph. It is completely unaware of which MongoDB collection an edge came from — it operates only on GraphNodeRef tuples and GraphEdge weights.

BFS Algorithm (core/bfs.ts)

bfsHeterogeneous(adjacency, opts) -> BfsPath[]

Key Design Choices

Parent-pointer reconstruction — paths are rebuilt by walking the parent chain from the found terminal state, eliminating the O(n) array spread that existed in the previous smartPathfindingService on every BFS step.

Per-depth visited cache — a node can be visited at depth N once. If a shorter path to the same node was already found at depth < N, the current branch is pruned. This allows diverse paths through popular hub nodes (e.g., a large company) without revisiting the same node at the same depth wastefully.

Parallel frontier expansion — all nodes at the current BFS depth are expanded in parallel via Promise.all. Adapter fan-out is bounded internally so this does not blow up memory.

Wall-clock timeout — configurable timeoutMs (default 8 seconds) aborts runaway searches before they exhaust the request lifecycle.

Transit type filterallowTransitNodeTypes restricts which node types may appear as intermediate hops. The source and target are always exempt from this filter.

Options

typescript
interface BfsOptions {
  source: GraphNodeRef;
  target: GraphNodeRef | ((n: GraphNodeRef) => boolean);
  maxDepth?: number;         // default 5
  maxPaths?: number;         // default 10
  allowTransitNodeTypes?: NodeType[];
  timeoutMs?: number;        // default 8000
}

target can be a predicate function to support "find any company in this industry" style searches.

AdjacencyService (core/AdjacencyService.ts)

Coordinates adapter fan-out. For each node being expanded, it calls all registered adapters and merges their edge lists. Adapters handle their own batching and caching to prevent N+1 queries.

Adapters

Each adapter knows how to expand one or more node types into outgoing GraphEdge objects.

AdapterSource Node TypesEdge KindNotes
personalConnectionAdapteruser, contactpersonal-contactReads from Connection collection; owner-scoped warmth/trust/recency
employmentAdapteruser, contact, company, branchemploymentEmits EMPLOYED_AT, REPRESENTS, REPRESENTED_BY, BRANCH_OF relations
companyRelAdaptercompanycompany-relReads CompanyRelationship; handles bidirectional types
knowledgeEdgeAdapterallknowledgeGeneric escape hatch for new entity types
userLinkAdapteruser, contactpersonal-contactCross-ref between User and Contact nodes that share an identity

Edge Scoring (scoring/edgeScoring.ts)

Single unified scorer replacing the previously divergent local vs smart scoring systems. All edge kinds return a value on the 0–100 scale.

Personal Contact Scoring

warmth = calculateWarmthDecay(rawWarmth, lastInteractionAt)
trust  = metadata.trustScore ?? 50
recency = calculateRecencyScore(lastInteractionAt)
score  = calculateEdgeScore(warmth, trust, recency, 50)

calculateWarmthDecay was previously defined in graphUtils.ts but never called. It is now wired in here.

Employment Scoring

REPRESENTS / REPRESENTED_BY (when isRepresentative = true):

  • Tier 0: falls through to regular employment math, max 90
  • Tier 1 (email-verified): 95 current, 70 past
  • Tier 2 (doc/vouch-verified): 100 current, 75 past

BRANCH_OF: flat 95 (near-free structural hop)

EMPLOYED_AT (regular employees):

  • Base: 100 (current) or 70→20 decaying by months since end date
  • Tenure bonus: up to +25 (5 points per year, capped)
  • Seniority lift: 0 (intern) → 25 (founder / c-level)
  • Company-admin verified bonus: +10
  • Multiplied by provenance.confidence
  • Hard cap at EMPLOYMENT_MAX = 90 — always strictly below structural edges

Company Relationship Scoring

score = base[type] × strength × recencyMultiplier

recencyMultiplier decays from 1.0 to 0.3 if the relationship has ended (5% per month, floored at 0.3).

Knowledge Edge Scoring

score = rawWeight × confidence

Path Scoring (scoring/pathScoring.ts)

Overall path likeliness is computed from the edge sequence using GraphUtils.calculatePathLikeliness. A single weak edge in the chain significantly reduces the overall score, which is the intended behavior — the system should prefer short paths with consistently warm edges over long paths with one very hot edge.

Performance Notes

  • O(n) Array.shift() in the old code is eliminated — BFS uses array indices with frontier / nextFrontier level-swapping
  • Adapter-level batching prevents N+1 MongoDB queries during fan-out
  • Parent-pointer reconstruction is O(depth), not O(path_count × depth)