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:
78
migrations/009_impact_analysis.sql
Normal file
78
migrations/009_impact_analysis.sql
Normal file
@@ -0,0 +1,78 @@
|
||||
-- Impact Analysis Schema
|
||||
-- Tracks system components, dependencies, and verification checks
|
||||
|
||||
-- Components registry
|
||||
CREATE TABLE IF NOT EXISTS components (
|
||||
id TEXT PRIMARY KEY,
|
||||
name TEXT NOT NULL,
|
||||
type TEXT NOT NULL CHECK (type IN ('service', 'script', 'config', 'database', 'api', 'ui', 'library')),
|
||||
path TEXT,
|
||||
repo TEXT,
|
||||
description TEXT,
|
||||
health_check TEXT,
|
||||
created_at TIMESTAMP DEFAULT NOW(),
|
||||
updated_at TIMESTAMP DEFAULT NOW()
|
||||
);
|
||||
|
||||
-- Component dependencies (directed graph)
|
||||
CREATE TABLE IF NOT EXISTS component_dependencies (
|
||||
id SERIAL PRIMARY KEY,
|
||||
component_id TEXT NOT NULL REFERENCES components(id) ON DELETE CASCADE,
|
||||
depends_on TEXT NOT NULL REFERENCES components(id) ON DELETE CASCADE,
|
||||
dependency_type TEXT NOT NULL CHECK (dependency_type IN ('hard', 'soft', 'config', 'data')),
|
||||
description TEXT,
|
||||
created_at TIMESTAMP DEFAULT NOW(),
|
||||
UNIQUE(component_id, depends_on)
|
||||
);
|
||||
|
||||
-- File-to-component mapping (for git diff analysis)
|
||||
CREATE TABLE IF NOT EXISTS component_files (
|
||||
id SERIAL PRIMARY KEY,
|
||||
component_id TEXT NOT NULL REFERENCES components(id) ON DELETE CASCADE,
|
||||
file_pattern TEXT NOT NULL,
|
||||
created_at TIMESTAMP DEFAULT NOW(),
|
||||
UNIQUE(component_id, file_pattern)
|
||||
);
|
||||
|
||||
-- Verification checks per component
|
||||
CREATE TABLE IF NOT EXISTS verification_checks (
|
||||
id SERIAL PRIMARY KEY,
|
||||
component_id TEXT NOT NULL REFERENCES components(id) ON DELETE CASCADE,
|
||||
name TEXT NOT NULL,
|
||||
check_type TEXT NOT NULL CHECK (check_type IN ('command', 'http', 'tcp', 'file')),
|
||||
check_command TEXT NOT NULL,
|
||||
expected_result TEXT,
|
||||
timeout_seconds INTEGER DEFAULT 30,
|
||||
created_at TIMESTAMP DEFAULT NOW()
|
||||
);
|
||||
|
||||
-- Historical change impacts (learned from errors)
|
||||
CREATE TABLE IF NOT EXISTS change_impacts (
|
||||
id SERIAL PRIMARY KEY,
|
||||
changed_component TEXT NOT NULL REFERENCES components(id) ON DELETE CASCADE,
|
||||
affected_component TEXT NOT NULL REFERENCES components(id) ON DELETE CASCADE,
|
||||
impact_description TEXT NOT NULL,
|
||||
error_id TEXT,
|
||||
task_id TEXT,
|
||||
learned_at TIMESTAMP DEFAULT NOW()
|
||||
);
|
||||
|
||||
-- Impact analysis runs (audit log)
|
||||
CREATE TABLE IF NOT EXISTS impact_analysis_runs (
|
||||
id SERIAL PRIMARY KEY,
|
||||
task_id TEXT,
|
||||
triggered_by TEXT NOT NULL CHECK (triggered_by IN ('task_close', 'manual', 'git_push')),
|
||||
components_analyzed INTEGER DEFAULT 0,
|
||||
issues_found INTEGER DEFAULT 0,
|
||||
verification_passed BOOLEAN,
|
||||
details JSONB,
|
||||
created_at TIMESTAMP DEFAULT NOW()
|
||||
);
|
||||
|
||||
-- Indexes for performance
|
||||
CREATE INDEX IF NOT EXISTS idx_component_deps_component ON component_dependencies(component_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_component_deps_depends ON component_dependencies(depends_on);
|
||||
CREATE INDEX IF NOT EXISTS idx_component_files_component ON component_files(component_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_change_impacts_changed ON change_impacts(changed_component);
|
||||
CREATE INDEX IF NOT EXISTS idx_change_impacts_affected ON change_impacts(affected_component);
|
||||
CREATE INDEX IF NOT EXISTS idx_impact_runs_task ON impact_analysis_runs(task_id);
|
||||
103
migrations/seed_components.sql
Normal file
103
migrations/seed_components.sql
Normal file
@@ -0,0 +1,103 @@
|
||||
-- Seed initial components for impact analysis
|
||||
-- Run after 009_impact_analysis.sql
|
||||
|
||||
-- Docker services on docker-host
|
||||
INSERT INTO components (id, name, type, path, repo, description, health_check) VALUES
|
||||
('propertymap-scraper', 'PropertyMap Scraper', 'service', '/opt/docker/propertymap', 'christian/propertymap', 'Scrapes Bazaraki/BuySellCyprus listings', 'docker inspect --format="{{.State.Health.Status}}" propertymap-scraper'),
|
||||
('propertymap-db', 'PropertyMap Database', 'database', 'litellm-pgvector', NULL, 'PostgreSQL with pgvector for property embeddings', 'docker exec litellm-pgvector pg_isready -U litellm'),
|
||||
('gridbot-conductor', 'Gridbot Conductor', 'service', '/opt/apps/eToroGridbot', 'christian/eToroGridbot', 'Trading signal processor and order executor', 'curl -s http://localhost:8000/health'),
|
||||
('litellm', 'LiteLLM Proxy', 'service', '/opt/docker/litellm', NULL, 'LLM API proxy with cost tracking', 'curl -s http://localhost:4000/health'),
|
||||
('n8n', 'n8n Workflows', 'service', '/opt/docker/n8n', NULL, 'Workflow automation', 'curl -s http://localhost:5678/healthz'),
|
||||
('gitea', 'Gitea', 'service', '/opt/docker/gitea', NULL, 'Git hosting and CI', 'curl -s http://localhost:3000/api/v1/version')
|
||||
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();
|
||||
|
||||
-- Local scripts (AgilitonScripts)
|
||||
INSERT INTO components (id, name, type, path, repo, description) VALUES
|
||||
('agiliton-scripts', 'AgilitonScripts', 'library', '~/Development/Infrastructure/AgilitonScripts', 'christian/AgilitonScripts', 'CLI tools and automation scripts'),
|
||||
('task-cli', 'Task CLI', 'script', '~/Development/Infrastructure/AgilitonScripts/bin/task', 'christian/AgilitonScripts', 'Task management CLI wrapper'),
|
||||
('backup-restic', 'Backup Restic', 'script', '~/Development/Infrastructure/AgilitonScripts/bin/backup-restic', 'christian/AgilitonScripts', 'Restic backup to Hetzner S3'),
|
||||
('vault-cli', 'Vault CLI', 'script', '~/Development/Infrastructure/AgilitonScripts/bin/vault', 'christian/AgilitonScripts', 'GPG-encrypted credential vault')
|
||||
ON CONFLICT (id) DO UPDATE SET
|
||||
name = EXCLUDED.name,
|
||||
path = EXCLUDED.path,
|
||||
repo = EXCLUDED.repo,
|
||||
description = EXCLUDED.description,
|
||||
updated_at = NOW();
|
||||
|
||||
-- MCP servers
|
||||
INSERT INTO components (id, name, type, path, repo, description) VALUES
|
||||
('task-mcp', 'Task MCP', 'service', '~/Development/Infrastructure/mcp-servers/task-mcp', NULL, 'Task management MCP server'),
|
||||
('gridbot-mcp', 'Gridbot MCP', 'api', 'docker-host:8000', 'christian/eToroGridbot', 'Trading operations MCP proxy'),
|
||||
('vault-mcp', 'Vault MCP', 'api', '~/bin/vault-mcp', 'christian/AgilitonScripts', 'Credential vault MCP server'),
|
||||
('gitea-mcp', 'Gitea MCP', 'api', '~/bin/gitea-mcp', NULL, 'Gitea operations MCP server')
|
||||
ON CONFLICT (id) DO UPDATE SET
|
||||
name = EXCLUDED.name,
|
||||
path = EXCLUDED.path,
|
||||
repo = EXCLUDED.repo,
|
||||
description = EXCLUDED.description,
|
||||
updated_at = NOW();
|
||||
|
||||
-- Configuration files
|
||||
INSERT INTO components (id, name, type, path, description) VALUES
|
||||
('claude-config', 'Claude Code Config', 'config', '~/.claude.json', 'MCP server and Claude Code configuration'),
|
||||
('ssh-tunnel-config', 'SSH Tunnel Config', 'config', '~/Library/LaunchAgents/eu.agiliton.ssh-tunnel.plist', 'autossh tunnels to docker-host'),
|
||||
('zshrc', 'Zsh Config', 'config', '~/.zshrc', 'Shell configuration and PATH')
|
||||
ON CONFLICT (id) DO UPDATE SET
|
||||
name = EXCLUDED.name,
|
||||
path = EXCLUDED.path,
|
||||
description = EXCLUDED.description,
|
||||
updated_at = NOW();
|
||||
|
||||
-- Dependencies
|
||||
INSERT INTO component_dependencies (component_id, depends_on, dependency_type, description) VALUES
|
||||
('propertymap-scraper', 'propertymap-db', 'hard', 'Stores scraped properties'),
|
||||
('propertymap-scraper', 'litellm', 'soft', 'Uses LLM for embeddings'),
|
||||
('gridbot-conductor', 'gridbot-mcp', 'config', 'MCP exposes API'),
|
||||
('task-mcp', 'propertymap-db', 'hard', 'Uses litellm-pgvector database'),
|
||||
('task-mcp', 'ssh-tunnel-config', 'hard', 'Needs port 5435 tunnel'),
|
||||
('task-cli', 'task-mcp', 'soft', 'CLI can use MCP or direct DB'),
|
||||
('agiliton-scripts', 'zshrc', 'config', 'PATH configuration'),
|
||||
('vault-mcp', 'vault-cli', 'hard', 'MCP wraps vault CLI'),
|
||||
('gitea-mcp', 'ssh-tunnel-config', 'hard', 'Needs port 3000 tunnel')
|
||||
ON CONFLICT (component_id, depends_on) DO UPDATE SET
|
||||
dependency_type = EXCLUDED.dependency_type,
|
||||
description = EXCLUDED.description;
|
||||
|
||||
-- File patterns for git diff analysis
|
||||
INSERT INTO component_files (component_id, file_pattern) VALUES
|
||||
('propertymap-scraper', 'propertymap/services/*.py'),
|
||||
('propertymap-scraper', 'propertymap/scraper/*.py'),
|
||||
('propertymap-scraper', 'propertymap/docker-compose.yml'),
|
||||
('gridbot-conductor', 'eToroGridbot/conductor/*.py'),
|
||||
('gridbot-conductor', 'eToroGridbot/docker-compose.yml'),
|
||||
('task-mcp', 'mcp-servers/task-mcp/src/*.ts'),
|
||||
('task-mcp', 'mcp-servers/task-mcp/migrations/*.sql'),
|
||||
('task-cli', 'AgilitonScripts/bin/task'),
|
||||
('agiliton-scripts', 'AgilitonScripts/bin/*'),
|
||||
('claude-config', '.claude.json'),
|
||||
('ssh-tunnel-config', 'Library/LaunchAgents/eu.agiliton.ssh-tunnel.plist'),
|
||||
('zshrc', '.zshrc')
|
||||
ON CONFLICT (component_id, file_pattern) DO NOTHING;
|
||||
|
||||
-- Verification checks
|
||||
INSERT INTO verification_checks (component_id, name, check_type, check_command, expected_result) VALUES
|
||||
('propertymap-scraper', 'container-health', 'command', 'ssh docker-host "docker inspect --format={{.State.Health.Status}} propertymap-scraper"', 'healthy'),
|
||||
('propertymap-db', 'db-ready', 'command', 'ssh docker-host "docker exec litellm-pgvector pg_isready -U litellm"', 'accepting connections'),
|
||||
('gridbot-conductor', 'api-health', 'http', 'http://localhost:8000/health', '{"status":"ok"}'),
|
||||
('task-mcp', 'db-connection', 'tcp', 'localhost:5435', 'connected'),
|
||||
('ssh-tunnel-config', 'tunnel-active', 'command', 'pgrep -f "ssh.*5435" || pgrep -f autossh', 'running')
|
||||
ON CONFLICT DO NOTHING;
|
||||
|
||||
-- Historical impacts (learned from this session)
|
||||
INSERT INTO change_impacts (changed_component, affected_component, impact_description, task_id) VALUES
|
||||
('propertymap-db', 'propertymap-scraper', 'pgvector migration required updating processor.py in services/ directory, not scraper/services/', NULL),
|
||||
('agiliton-scripts', 'backup-restic', 'Path changed from ~/Development/AgilitonScripts to ~/Development/Infrastructure/AgilitonScripts', NULL),
|
||||
('task-mcp', 'task-cli', 'CLI uses project name in INSERT but database expects project key - foreign key violation', NULL)
|
||||
ON CONFLICT DO NOTHING;
|
||||
1860
package-lock.json
generated
Normal file
1860
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
54
src/index.ts
54
src/index.ts
@@ -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
311
src/tools/impact.ts
Normal 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 };
|
||||
}
|
||||
@@ -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)' },
|
||||
},
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
Reference in New Issue
Block a user