feat: Add impact analysis for component dependency tracking

- New tables: components, component_dependencies, component_files,
  verification_checks, change_impacts, impact_analysis_runs
- 8 new MCP tools: component_register, component_list,
  component_add_dependency, component_add_file, component_add_check,
  impact_analysis, impact_learn, component_graph
- Seed data: 17 components, 9 dependencies, 12 file patterns, 5 checks
- Historical impacts from session 397 issues recorded

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Christian Gick
2026-01-11 07:20:00 +02:00
parent 5015b1416f
commit 4fb557c624
6 changed files with 2517 additions and 0 deletions

View File

@@ -26,6 +26,16 @@ import { taskDelegations, taskDelegationQuery } from './tools/delegations.js';
import { projectLock, projectUnlock, projectLockStatus, projectContext } from './tools/locks.js';
import { versionAdd, versionList, versionShow, versionUpdate, versionRelease, versionAssignTask } from './tools/versions.js';
import { taskCommitAdd, taskCommitRemove, taskCommitsList, taskLinkCommits, sessionTasks } from './tools/commits.js';
import {
componentRegister,
componentList,
componentAddDependency,
componentAddFile,
componentAddCheck,
impactAnalysis,
impactLearn,
componentGraph,
} from './tools/impact.js';
// Create MCP server
const server = new Server(
@@ -266,6 +276,50 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
});
break;
// Impact Analysis
case 'component_register':
result = JSON.stringify(await componentRegister(a.id, a.name, a.type, {
path: a.path,
repo: a.repo,
description: a.description,
health_check: a.health_check,
}), null, 2);
break;
case 'component_list':
result = JSON.stringify(await componentList(a.type), null, 2);
break;
case 'component_add_dependency':
result = JSON.stringify(await componentAddDependency(
a.component_id,
a.depends_on,
a.dependency_type,
a.description
), null, 2);
break;
case 'component_add_file':
result = JSON.stringify(await componentAddFile(a.component_id, a.file_pattern), null, 2);
break;
case 'component_add_check':
result = JSON.stringify(await componentAddCheck(a.component_id, a.name, a.check_type, a.check_command, {
expected_result: a.expected_result,
timeout_seconds: a.timeout_seconds,
}), null, 2);
break;
case 'impact_analysis':
result = JSON.stringify(await impactAnalysis(a.changed_files), null, 2);
break;
case 'impact_learn':
result = JSON.stringify(await impactLearn(
a.changed_component,
a.affected_component,
a.impact_description,
{ error_id: a.error_id, task_id: a.task_id }
), null, 2);
break;
case 'component_graph':
result = JSON.stringify(await componentGraph(a.component_id), null, 2);
break;
default:
throw new Error(`Unknown tool: ${name}`);
}

311
src/tools/impact.ts Normal file
View File

@@ -0,0 +1,311 @@
// Impact Analysis Tools
// Track components, dependencies, and verify changes
import { query, queryOne, execute } from '../db.js';
interface Component {
id: string;
name: string;
type: string;
path?: string;
repo?: string;
description?: string;
health_check?: string;
}
interface ComponentDependency {
id: number;
component_id: string;
depends_on: string;
dependency_type: string;
description?: string;
}
interface VerificationCheck {
id: number;
component_id: string;
name: string;
check_type: string;
check_command: string;
expected_result?: string;
timeout_seconds: number;
}
/**
* Register a new component
*/
export async function componentRegister(
id: string,
name: string,
type: string,
options: {
path?: string;
repo?: string;
description?: string;
health_check?: string;
} = {}
): Promise<{ success: boolean; component: Component }> {
await execute(
`INSERT INTO components (id, name, type, path, repo, description, health_check)
VALUES ($1, $2, $3, $4, $5, $6, $7)
ON CONFLICT (id) DO UPDATE SET
name = EXCLUDED.name,
type = EXCLUDED.type,
path = EXCLUDED.path,
repo = EXCLUDED.repo,
description = EXCLUDED.description,
health_check = EXCLUDED.health_check,
updated_at = NOW()`,
[id, name, type, options.path || null, options.repo || null, options.description || null, options.health_check || null]
);
const component = await queryOne<Component>(
'SELECT * FROM components WHERE id = $1',
[id]
);
return { success: true, component: component! };
}
/**
* List all components
*/
export async function componentList(
type?: string
): Promise<Component[]> {
if (type) {
return query<Component>(
'SELECT * FROM components WHERE type = $1 ORDER BY name',
[type]
);
}
return query<Component>(
'SELECT * FROM components ORDER BY type, name'
);
}
/**
* Add a dependency between components
*/
export async function componentAddDependency(
component_id: string,
depends_on: string,
dependency_type: string,
description?: string
): Promise<{ success: boolean; message: string }> {
// Verify both components exist
const source = await queryOne<Component>('SELECT id FROM components WHERE id = $1', [component_id]);
const target = await queryOne<Component>('SELECT id FROM components WHERE id = $1', [depends_on]);
if (!source) return { success: false, message: `Component ${component_id} not found` };
if (!target) return { success: false, message: `Component ${depends_on} not found` };
await execute(
`INSERT INTO component_dependencies (component_id, depends_on, dependency_type, description)
VALUES ($1, $2, $3, $4)
ON CONFLICT (component_id, depends_on) DO UPDATE SET
dependency_type = EXCLUDED.dependency_type,
description = EXCLUDED.description`,
[component_id, depends_on, dependency_type, description || null]
);
return { success: true, message: `Added dependency: ${component_id} -> ${depends_on} (${dependency_type})` };
}
/**
* Add file pattern to component mapping
*/
export async function componentAddFile(
component_id: string,
file_pattern: string
): Promise<{ success: boolean; message: string }> {
const component = await queryOne<Component>('SELECT id FROM components WHERE id = $1', [component_id]);
if (!component) return { success: false, message: `Component ${component_id} not found` };
await execute(
`INSERT INTO component_files (component_id, file_pattern)
VALUES ($1, $2)
ON CONFLICT (component_id, file_pattern) DO NOTHING`,
[component_id, file_pattern]
);
return { success: true, message: `Added file pattern ${file_pattern} to ${component_id}` };
}
/**
* Add verification check to component
*/
export async function componentAddCheck(
component_id: string,
name: string,
check_type: string,
check_command: string,
options: { expected_result?: string; timeout_seconds?: number } = {}
): Promise<{ success: boolean; check_id: number }> {
const component = await queryOne<Component>('SELECT id FROM components WHERE id = $1', [component_id]);
if (!component) throw new Error(`Component ${component_id} not found`);
const result = await queryOne<{ id: number }>(
`INSERT INTO verification_checks (component_id, name, check_type, check_command, expected_result, timeout_seconds)
VALUES ($1, $2, $3, $4, $5, $6)
RETURNING id`,
[component_id, name, check_type, check_command, options.expected_result || null, options.timeout_seconds || 30]
);
return { success: true, check_id: result!.id };
}
/**
* Analyze impact of changes to files
*/
export async function impactAnalysis(
changed_files: string[]
): Promise<{
affected_components: string[];
downstream_components: string[];
verification_checks: VerificationCheck[];
historical_issues: Array<{ component: string; description: string }>;
}> {
const affected = new Set<string>();
const downstream = new Set<string>();
// Find components affected by file changes
for (const file of changed_files) {
const matches = await query<{ component_id: string }>(
`SELECT DISTINCT component_id FROM component_files
WHERE $1 LIKE REPLACE(file_pattern, '*', '%')
OR $1 = file_pattern`,
[file]
);
for (const m of matches) {
affected.add(m.component_id);
}
}
// Find downstream dependencies (what depends on affected components)
for (const comp of affected) {
const deps = await query<{ component_id: string }>(
`WITH RECURSIVE downstream AS (
SELECT component_id FROM component_dependencies WHERE depends_on = $1
UNION
SELECT cd.component_id FROM component_dependencies cd
JOIN downstream d ON cd.depends_on = d.component_id
)
SELECT component_id FROM downstream`,
[comp]
);
for (const d of deps) {
downstream.add(d.component_id);
}
}
// Get verification checks for affected + downstream
const allAffected = [...affected, ...downstream];
const checks: VerificationCheck[] = allAffected.length > 0
? await query<VerificationCheck>(
`SELECT * FROM verification_checks WHERE component_id = ANY($1)`,
[allAffected]
)
: [];
// Get historical issues
const historicalIssues = allAffected.length > 0
? await query<{ component: string; description: string }>(
`SELECT affected_component as component, impact_description as description
FROM change_impacts
WHERE changed_component = ANY($1)
ORDER BY learned_at DESC
LIMIT 10`,
[[...affected]]
)
: [];
return {
affected_components: [...affected],
downstream_components: [...downstream],
verification_checks: checks,
historical_issues: historicalIssues,
};
}
/**
* Record a learned impact (from error discovery)
*/
export async function impactLearn(
changed_component: string,
affected_component: string,
impact_description: string,
options: { error_id?: string; task_id?: string } = {}
): Promise<{ success: boolean; id: number }> {
const result = await queryOne<{ id: number }>(
`INSERT INTO change_impacts (changed_component, affected_component, impact_description, error_id, task_id)
VALUES ($1, $2, $3, $4, $5)
RETURNING id`,
[changed_component, affected_component, impact_description, options.error_id || null, options.task_id || null]
);
return { success: true, id: result!.id };
}
/**
* Get component dependency graph
*/
export async function componentGraph(
component_id?: string
): Promise<{
nodes: Array<{ id: string; name: string; type: string }>;
edges: Array<{ from: string; to: string; type: string }>;
}> {
const nodes = component_id
? await query<{ id: string; name: string; type: string }>(
`WITH RECURSIVE deps AS (
SELECT id, name, type FROM components WHERE id = $1
UNION
SELECT c.id, c.name, c.type FROM components c
JOIN component_dependencies cd ON c.id = cd.depends_on
JOIN deps d ON cd.component_id = d.id
UNION
SELECT c.id, c.name, c.type FROM components c
JOIN component_dependencies cd ON c.id = cd.component_id
JOIN deps d ON cd.depends_on = d.id
)
SELECT DISTINCT id, name, type FROM deps`,
[component_id]
)
: await query<{ id: string; name: string; type: string }>(
'SELECT id, name, type FROM components'
);
const nodeIds = nodes.map(n => n.id);
const edges = nodeIds.length > 0
? await query<{ from: string; to: string; type: string }>(
`SELECT component_id as "from", depends_on as "to", dependency_type as type
FROM component_dependencies
WHERE component_id = ANY($1) AND depends_on = ANY($1)`,
[nodeIds]
)
: [];
return { nodes, edges };
}
/**
* Log an impact analysis run
*/
export async function logImpactRun(
triggered_by: string,
components_analyzed: number,
issues_found: number,
verification_passed: boolean,
details: object,
task_id?: string
): Promise<{ id: number }> {
const result = await queryOne<{ id: number }>(
`INSERT INTO impact_analysis_runs (task_id, triggered_by, components_analyzed, issues_found, verification_passed, details)
VALUES ($1, $2, $3, $4, $5, $6)
RETURNING id`,
[task_id || null, triggered_by, components_analyzed, issues_found, verification_passed, JSON.stringify(details)]
);
return { id: result!.id };
}

View File

@@ -435,4 +435,115 @@ export const toolDefinitions = [
properties: {},
},
},
// Impact Analysis Tools
{
name: 'component_register',
description: 'Register a system component for impact analysis tracking',
inputSchema: {
type: 'object',
properties: {
id: { type: 'string', description: 'Unique component ID (e.g., propertymap-scraper, gridbot-conductor)' },
name: { type: 'string', description: 'Human-readable name' },
type: { type: 'string', enum: ['service', 'script', 'config', 'database', 'api', 'ui', 'library'], description: 'Component type' },
path: { type: 'string', description: 'File system path or Docker container name' },
repo: { type: 'string', description: 'Git repository (e.g., christian/propertymap)' },
description: { type: 'string', description: 'What this component does' },
health_check: { type: 'string', description: 'Command or URL to check health' },
},
required: ['id', 'name', 'type'],
},
},
{
name: 'component_list',
description: 'List registered components, optionally filtered by type',
inputSchema: {
type: 'object',
properties: {
type: { type: 'string', enum: ['service', 'script', 'config', 'database', 'api', 'ui', 'library'], description: 'Filter by component type' },
},
},
},
{
name: 'component_add_dependency',
description: 'Add a dependency between two components',
inputSchema: {
type: 'object',
properties: {
component_id: { type: 'string', description: 'Source component ID' },
depends_on: { type: 'string', description: 'Target component ID (what source depends on)' },
dependency_type: { type: 'string', enum: ['hard', 'soft', 'config', 'data'], description: 'Type of dependency' },
description: { type: 'string', description: 'Description of the dependency' },
},
required: ['component_id', 'depends_on', 'dependency_type'],
},
},
{
name: 'component_add_file',
description: 'Map a file pattern to a component (for git diff analysis)',
inputSchema: {
type: 'object',
properties: {
component_id: { type: 'string', description: 'Component ID' },
file_pattern: { type: 'string', description: 'File pattern (e.g., src/services/*.py, docker-compose.yml)' },
},
required: ['component_id', 'file_pattern'],
},
},
{
name: 'component_add_check',
description: 'Add a verification check to a component',
inputSchema: {
type: 'object',
properties: {
component_id: { type: 'string', description: 'Component ID' },
name: { type: 'string', description: 'Check name (e.g., health-endpoint, container-running)' },
check_type: { type: 'string', enum: ['command', 'http', 'tcp', 'file'], description: 'Type of check' },
check_command: { type: 'string', description: 'Command/URL to execute' },
expected_result: { type: 'string', description: 'Expected output or status' },
timeout_seconds: { type: 'number', description: 'Timeout in seconds (default: 30)' },
},
required: ['component_id', 'name', 'check_type', 'check_command'],
},
},
{
name: 'impact_analysis',
description: 'Analyze which components are affected by file changes',
inputSchema: {
type: 'object',
properties: {
changed_files: {
type: 'array',
items: { type: 'string' },
description: 'List of changed file paths',
},
},
required: ['changed_files'],
},
},
{
name: 'impact_learn',
description: 'Record a learned impact relationship (when we discover a missed dependency)',
inputSchema: {
type: 'object',
properties: {
changed_component: { type: 'string', description: 'Component that was changed' },
affected_component: { type: 'string', description: 'Component that was unexpectedly affected' },
impact_description: { type: 'string', description: 'What went wrong' },
error_id: { type: 'string', description: 'Related error ID from error memory' },
task_id: { type: 'string', description: 'Related task ID' },
},
required: ['changed_component', 'affected_component', 'impact_description'],
},
},
{
name: 'component_graph',
description: 'Get component dependency graph (for visualization)',
inputSchema: {
type: 'object',
properties: {
component_id: { type: 'string', description: 'Center component (optional, shows all if omitted)' },
},
},
},
];