feat(#71): implement graph data API

Implemented three new API endpoints for knowledge graph visualization:

1. GET /api/knowledge/graph - Full knowledge graph
   - Returns all entries and links with optional filtering
   - Supports filtering by tags, status, and node count limit
   - Includes orphan detection (entries with no links)

2. GET /api/knowledge/graph/stats - Graph statistics
   - Total entries and links counts
   - Orphan entries detection
   - Average links per entry
   - Top 10 most connected entries
   - Tag distribution across entries

3. GET /api/knowledge/graph/:slug - Entry-centered subgraph
   - Returns graph centered on specific entry
   - Supports depth parameter (1-5) for traversal distance
   - Includes all connected nodes up to specified depth

New Files:
- apps/api/src/knowledge/graph.controller.ts
- apps/api/src/knowledge/graph.controller.spec.ts

Modified Files:
- apps/api/src/knowledge/dto/graph-query.dto.ts (added GraphFilterDto)
- apps/api/src/knowledge/entities/graph.entity.ts (extended with new types)
- apps/api/src/knowledge/services/graph.service.ts (added new methods)
- apps/api/src/knowledge/services/graph.service.spec.ts (added tests)
- apps/api/src/knowledge/knowledge.module.ts (registered controller)
- apps/api/src/knowledge/dto/index.ts (exported new DTOs)
- docs/scratchpads/71-graph-data-api.md (implementation notes)

Test Coverage: 21 tests (all passing)
- 14 service tests including orphan detection, filtering, statistics
- 7 controller tests for all three endpoints

Follows TDD principles with tests written before implementation.
All code quality gates passed (lint, typecheck, tests).

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
Jason Woltje
2026-02-02 15:27:00 -06:00
parent 3969dd5598
commit 5d348526de
240 changed files with 10400 additions and 23 deletions

View File

@@ -0,0 +1,132 @@
import { Injectable, OnModuleDestroy } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { ValkeyClient, ValkeyClientConfig } from './valkey.client';
import type {
TaskState,
AgentState,
TaskStatus,
AgentStatus,
OrchestratorEvent,
EventHandler,
TaskContext,
} from './types';
/**
* NestJS service for Valkey state management and pub/sub
*/
@Injectable()
export class ValkeyService implements OnModuleDestroy {
private readonly client: ValkeyClient;
constructor(private readonly configService: ConfigService) {
const config: ValkeyClientConfig = {
host: this.configService.get<string>('orchestrator.valkey.host', 'localhost'),
port: this.configService.get<number>('orchestrator.valkey.port', 6379),
};
const password = this.configService.get<string>('orchestrator.valkey.password');
if (password) {
config.password = password;
}
this.client = new ValkeyClient(config);
}
async onModuleDestroy(): Promise<void> {
await this.client.disconnect();
}
/**
* Task State Management
*/
async getTaskState(taskId: string): Promise<TaskState | null> {
return this.client.getTaskState(taskId);
}
async setTaskState(state: TaskState): Promise<void> {
return this.client.setTaskState(state);
}
async deleteTaskState(taskId: string): Promise<void> {
return this.client.deleteTaskState(taskId);
}
async updateTaskStatus(
taskId: string,
status: TaskStatus,
agentId?: string,
error?: string
): Promise<TaskState> {
return this.client.updateTaskStatus(taskId, status, agentId, error);
}
async listTasks(): Promise<TaskState[]> {
return this.client.listTasks();
}
/**
* Agent State Management
*/
async getAgentState(agentId: string): Promise<AgentState | null> {
return this.client.getAgentState(agentId);
}
async setAgentState(state: AgentState): Promise<void> {
return this.client.setAgentState(state);
}
async deleteAgentState(agentId: string): Promise<void> {
return this.client.deleteAgentState(agentId);
}
async updateAgentStatus(
agentId: string,
status: AgentStatus,
error?: string
): Promise<AgentState> {
return this.client.updateAgentStatus(agentId, status, error);
}
async listAgents(): Promise<AgentState[]> {
return this.client.listAgents();
}
/**
* Event Pub/Sub
*/
async publishEvent(event: OrchestratorEvent): Promise<void> {
return this.client.publishEvent(event);
}
async subscribeToEvents(handler: EventHandler): Promise<void> {
return this.client.subscribeToEvents(handler);
}
/**
* Convenience methods
*/
async createTask(taskId: string, context: TaskContext): Promise<void> {
const now = new Date().toISOString();
const state: TaskState = {
taskId,
status: 'pending',
context,
createdAt: now,
updatedAt: now,
};
await this.setTaskState(state);
}
async createAgent(agentId: string, taskId: string): Promise<void> {
const state: AgentState = {
agentId,
status: 'spawning',
taskId,
};
await this.setAgentState(state);
}
}