feat(graph/subgraph-and-validation): implement subgraph and validation methods

- Add subgraph() method using graphology-operators.subgraph (ADR-007: internal-only edges)
- Add validateSchema() using TypeBox Value.Check/Value.Errors
- Add validateGraph() detecting cycles and dangling references
- Add validate() combining both validations
- Define ValidationError, GraphValidationError, AnyValidationError types in error module
- Add standalone validation functions in src/graph/validation.ts
- Export validation module from src/graph/index.ts
- Add 43 unit tests for subgraph filtering and validation
This commit is contained in:
2026-04-27 12:41:51 +00:00
parent b0d943f4e6
commit c3649256cc
6 changed files with 869 additions and 5 deletions

View File

@@ -1,6 +1,7 @@
// TaskGraph class construction — fromTasks, fromRecords, fromJSON, incremental building
import { DirectedGraph } from 'graphology';
import { subgraph as graphologySubgraph } from 'graphology-operators';
import { Value } from '@alkdev/typebox/value';
import type {
TaskGraphNodeAttributes,
@@ -15,6 +16,9 @@ import {
DuplicateEdgeError,
TaskNotFoundError,
InvalidInputError,
type ValidationError,
type GraphValidationError,
type AnyValidationError,
} from '../error/index.js';
import {
removeTask as _removeTask,
@@ -31,6 +35,11 @@ import {
taskCount as _taskCount,
getTask as _getTask,
} from './queries.js';
import {
validateSchema as _validateSchema,
validateGraph as _validateGraph,
validate as _validate,
} from './validation.js';
/**
* Internal graph type alias for the graphology DirectedGraph with our attribute types.
@@ -575,4 +584,91 @@ export class TaskGraph {
getTask(taskId: string): TaskGraphNodeAttributes | undefined {
return _getTask(this._graph, taskId);
}
// ---------------------------------------------------------------------------
// Subgraph method
// ---------------------------------------------------------------------------
/**
* Extract a subgraph containing only nodes that pass the filter predicate.
*
* Per ADR-007, returns only edges where **both endpoints** are in the
* filtered set (internal-only). External edges (where only one endpoint
* matches) are excluded. This produces a valid (potentially disconnected)
* subgraph suitable for all graph algorithms.
*
* Uses `graphology-operators.subgraph` under the hood, which preserves
* node and edge attributes.
*
* Does not mutate the original graph — returns a new `TaskGraph` instance.
*
* @param filter - Predicate function receiving taskId and attributes for each node
* @returns A new TaskGraph instance with matching nodes and internal-only edges
*/
subgraph(filter: (taskId: string, attrs: TaskGraphNodeAttributes) => boolean): TaskGraph {
// Build the set of node keys that pass the filter
const filteredNodes = new Set<string>();
for (const node of this._graph.nodes()) {
const attrs = this._graph.getNodeAttributes(node);
if (filter(node, attrs)) {
filteredNodes.add(node);
}
}
// Use graphology-operators subgraph which only keeps edges where
// both endpoints are in the filtered set (internal-only per ADR-007)
const subGraph = graphologySubgraph(this._graph, filteredNodes);
// Create a new TaskGraph and transfer the subgraph data
const result = new TaskGraph();
result._graph.import(subGraph.export());
return result;
}
// ---------------------------------------------------------------------------
// Validation methods
// ---------------------------------------------------------------------------
/**
* Validate all node attributes against the TaskGraphNodeAttributes schema.
*
* Uses TypeBox `Value.Check()` and `Value.Errors()` on each node's attributes.
* Returns structured `ValidationError[]` with `type: "schema"`, `taskId`,
* `field`, `message`, and optional `value`.
*
* Validation never throws — it collects all issues and returns them.
* This allows consumers to implement "collect all errors" strategies.
*/
validateSchema(): ValidationError[] {
return _validateSchema(this._graph);
}
/**
* Validate graph-level invariants: cycles and dangling references.
*
* Runs `findCycles()` and checks for dangling dependency references
* (edges where one endpoint doesn't exist as a node).
*
* Returns structured `GraphValidationError[]` with:
* - `type: "graph"`
* - `category: "cycle"` for cycle errors, with cycle paths in `details`
* - `category: "dangling-reference"` for dangling references, with `taskId`
*
* Validation never throws — it collects all issues and returns them.
*/
validateGraph(): GraphValidationError[] {
return _validateGraph(this._graph);
}
/**
* Run both schema and graph validation, returning combined results.
*
* Convenience method that runs `validateSchema()` and `validateGraph()`
* and concatenates the results into a single array.
*
* Validation never throws — it collects all issues and returns them.
*/
validate(): AnyValidationError[] {
return _validate(this._graph);
}
}