secufusion-mcp 2.5.0 → 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,74 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import { performFullExtraction } from "./server.js";
4
+ import { addTraceabilityLink, traceForward, findTraceabilityGaps, loadTraceability, saveTraceability } from "./traceability_engine.js";
5
+ import { saveRequirement } from "./requirement_manager.js";
6
+ const ecosystemRoot = "c:\\Users\\Yash\\Desktop\\mcp";
7
+ function setupRequirement(id, title) {
8
+ const req = {
9
+ id,
10
+ title,
11
+ status: "CONFIRMED",
12
+ raw_request: title,
13
+ clarification_questions: []
14
+ };
15
+ saveRequirement(ecosystemRoot, req);
16
+ }
17
+ function runRealBenchmark() {
18
+ console.log("=== RUNNING REAL REPOSITORY TRACEABILITY BENCHMARK ===");
19
+ // 1. Extract Real Repository
20
+ console.log("Extracting actual repository...");
21
+ const stats = performFullExtraction(ecosystemRoot);
22
+ console.log("Extraction stats:", JSON.stringify(stats));
23
+ const dnaPath = path.join(ecosystemRoot, ".secufusion", "dna.json");
24
+ const dna = JSON.parse(fs.readFileSync(dnaPath, "utf-8"));
25
+ const graph = dna.graph;
26
+ // Clear previous traceability
27
+ saveTraceability(ecosystemRoot, []);
28
+ // --- Retention Benchmark ---
29
+ console.log("\\n--- RETENTION BENCHMARK (REAL REPO) ---");
30
+ setupRequirement("REQ-RET-1", "Retention Policy");
31
+ const retentionNodes = graph.nodes.filter((n) => (n.name || "").toLowerCase().includes("retention"));
32
+ for (const node of retentionNodes) {
33
+ addTraceabilityLink(ecosystemRoot, "REQUIREMENT:REQ-RET-1", node.id, "IMPLEMENTED_BY", "INFERRED", "SUPPORTED");
34
+ }
35
+ const edges1 = loadTraceability(ecosystemRoot);
36
+ const traceRet = traceForward(graph, edges1, "REQUIREMENT:REQ-RET-1", 3);
37
+ const gapsRet = findTraceabilityGaps(ecosystemRoot, graph, edges1);
38
+ console.log("Traced Artifacts:", traceRet.nodes.length - 1);
39
+ console.log("Evidence Found:", traceRet.nodes.filter((n) => n.type !== "UNKNOWN" && !n.id.startsWith("REQUIREMENT:")).length);
40
+ console.log("Traceability Gaps (Retention):", gapsRet.filter((g) => g.id === "REQUIREMENT:REQ-RET-1").length > 0 ? "YES (REQUIREMENT_WITHOUT_IMPLEMENTATION)" : "NO");
41
+ console.log("Affected Services:", traceRet.nodes.filter((n) => n.type === "SERVICE").map((n) => n.name));
42
+ console.log("Affected APIs:", traceRet.nodes.filter((n) => n.type === "API").map((n) => n.name));
43
+ console.log("Affected DBs:", traceRet.nodes.filter((n) => n.type === "DATABASE_TABLE").map((n) => n.name));
44
+ // --- User Deletion Benchmark ---
45
+ console.log("\\n--- USER DELETION BENCHMARK (REAL REPO) ---");
46
+ setupRequirement("REQ-DEL-1", "User Deletion");
47
+ const userNodes = graph.nodes.filter((n) => (n.name || "").toLowerCase().includes("user"));
48
+ for (const node of userNodes) {
49
+ addTraceabilityLink(ecosystemRoot, "REQUIREMENT:REQ-DEL-1", node.id, "IMPLEMENTED_BY", "INFERRED", "SUPPORTED");
50
+ }
51
+ const edges2 = loadTraceability(ecosystemRoot);
52
+ const traceDel = traceForward(graph, edges2, "REQUIREMENT:REQ-DEL-1", 3);
53
+ const gapsDel = findTraceabilityGaps(ecosystemRoot, graph, edges2);
54
+ console.log("Traced Artifacts:", traceDel.nodes.length - 1);
55
+ console.log("Evidence Found:", traceDel.nodes.filter((n) => n.type !== "UNKNOWN" && !n.id.startsWith("REQUIREMENT:")).length);
56
+ console.log("Traceability Gaps (User Deletion):", gapsDel.filter((g) => g.id === "REQUIREMENT:REQ-DEL-1").length > 0 ? "YES (REQUIREMENT_WITHOUT_IMPLEMENTATION)" : "NO");
57
+ console.log("Affected Services:", traceDel.nodes.filter((n) => n.type === "SERVICE").map((n) => n.name));
58
+ console.log("Affected APIs:", traceDel.nodes.filter((n) => n.type === "API").map((n) => n.name));
59
+ console.log("\\n=== VALIDATION SUMMARY ===");
60
+ console.log("Did the MCP discover the actual SecuFusion implementation surface of a feature?");
61
+ if (retentionNodes.length === 0 && userNodes.length === 0) {
62
+ console.log("No. The actual SecuFusion backend repository (Java/Spring) is missing from this workspace. The extraction correctly resulted in 0 mappings without fabricating evidence. This correctly triggered the TRACEABILITY_GAP behavior.");
63
+ }
64
+ else {
65
+ console.log("Yes. Discovered actual implementation surface.");
66
+ }
67
+ }
68
+ try {
69
+ runRealBenchmark();
70
+ }
71
+ catch (e) {
72
+ console.error("Benchmark failed:", e);
73
+ process.exit(1);
74
+ }
@@ -0,0 +1,226 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ function migrateOldRecord(record) {
4
+ return {
5
+ id: record.id,
6
+ title: record.title || "Untitled",
7
+ summary: record.summary || record.description || "",
8
+ category: record.category || "OTHER",
9
+ lesson: record.lesson || record.description || "",
10
+ context: record.context || "",
11
+ trigger: record.trigger || "UNKNOWN",
12
+ outcome: record.outcome || "UNKNOWN",
13
+ confidence: record.confidence || "UNKNOWN",
14
+ status: record.status || "VALIDATED", // legacy records assume validated
15
+ created_at: record.created_at || new Date().toISOString(),
16
+ updated_at: record.updated_at || new Date().toISOString(),
17
+ related_artifacts: record.related_artifacts || [],
18
+ source_requirement_id: record.source_requirement_id,
19
+ source_plan_id: record.source_plan_id,
20
+ source_validation_id: record.source_validation_id,
21
+ source_test_result_ids: record.source_test_result_ids,
22
+ source_traceability_ids: record.source_traceability_ids,
23
+ repository_snapshot: record.repository_snapshot,
24
+ affected_services: record.affected_services,
25
+ affected_components: record.affected_components,
26
+ affected_apis: record.affected_apis,
27
+ affected_events: record.affected_events,
28
+ affected_tables: record.affected_tables,
29
+ evidence: record.evidence
30
+ };
31
+ }
32
+ export function loadExperience(ecosystemRoot) {
33
+ const p = path.join(ecosystemRoot, '.secufusion', 'experience.json');
34
+ if (!fs.existsSync(p))
35
+ return [];
36
+ try {
37
+ const data = JSON.parse(fs.readFileSync(p, 'utf-8'));
38
+ if (!Array.isArray(data))
39
+ return [];
40
+ return data.map(record => migrateOldRecord(record));
41
+ }
42
+ catch {
43
+ return [];
44
+ }
45
+ }
46
+ export function saveExperience(ecosystemRoot, experienceList) {
47
+ const dir = path.join(ecosystemRoot, '.secufusion');
48
+ if (!fs.existsSync(dir))
49
+ fs.mkdirSync(dir, { recursive: true });
50
+ fs.writeFileSync(path.join(dir, 'experience.json'), JSON.stringify(experienceList, null, 2));
51
+ }
52
+ export function recordExperience(ecosystemRoot, exp) {
53
+ const list = loadExperience(ecosystemRoot);
54
+ const now = new Date().toISOString();
55
+ // Check if updating existing
56
+ const existingIndex = list.findIndex(e => e.id === exp.id);
57
+ if (existingIndex >= 0) {
58
+ const updated = {
59
+ ...list[existingIndex],
60
+ ...exp,
61
+ updated_at: now
62
+ };
63
+ list[existingIndex] = updated;
64
+ saveExperience(ecosystemRoot, list);
65
+ return updated;
66
+ }
67
+ const newExp = {
68
+ id: exp.id || `EXP-${Date.now()}`,
69
+ title: exp.title || "Untitled",
70
+ summary: exp.summary || "",
71
+ category: exp.category || "OTHER",
72
+ lesson: exp.lesson || "",
73
+ context: exp.context || "",
74
+ trigger: exp.trigger || "UNKNOWN",
75
+ outcome: exp.outcome || "UNKNOWN",
76
+ confidence: exp.confidence || "UNKNOWN",
77
+ status: exp.status || "PROPOSED",
78
+ created_at: exp.created_at || now,
79
+ updated_at: now,
80
+ ...exp
81
+ };
82
+ list.push(newExp);
83
+ saveExperience(ecosystemRoot, list);
84
+ return newExp;
85
+ }
86
+ export function getEngineeringExperience(ecosystemRoot, query, contextFilters = {}) {
87
+ const list = loadExperience(ecosystemRoot);
88
+ // Bounded deterministic retrieval
89
+ let matched = list.filter(exp => {
90
+ let matches = false;
91
+ if (exp.status === "RETIRED")
92
+ return false;
93
+ // Exact artifact relationships
94
+ if (contextFilters.affected_components && exp.affected_components) {
95
+ matches = matches || exp.affected_components.some(c => contextFilters.affected_components.includes(c));
96
+ }
97
+ if (contextFilters.affected_services && exp.affected_services) {
98
+ matches = matches || exp.affected_services.some(s => contextFilters.affected_services.includes(s));
99
+ }
100
+ if (contextFilters.source_requirement_id && exp.source_requirement_id === contextFilters.source_requirement_id) {
101
+ matches = true;
102
+ }
103
+ // Keyword similarity (fallback)
104
+ if (query) {
105
+ const q = query.toLowerCase();
106
+ if (exp.title.toLowerCase().includes(q) || exp.summary.toLowerCase().includes(q) || exp.category.toLowerCase().includes(q)) {
107
+ matches = true;
108
+ }
109
+ }
110
+ return matches;
111
+ });
112
+ // Bound the results
113
+ return matched.slice(0, 5);
114
+ }
115
+ export function findExperienceConflicts(ecosystemRoot) {
116
+ const list = loadExperience(ecosystemRoot);
117
+ // Simple mock detection for Phase 6
118
+ // E.g. two experiences affecting same component with different outcomes or lessons
119
+ const conflicting = [];
120
+ const componentMap = new Map();
121
+ for (const exp of list) {
122
+ if (exp.status === "VALIDATED" && exp.affected_components) {
123
+ for (const c of exp.affected_components) {
124
+ if (!componentMap.has(c))
125
+ componentMap.set(c, []);
126
+ componentMap.get(c).push(exp);
127
+ }
128
+ }
129
+ }
130
+ for (const [comp, exps] of componentMap.entries()) {
131
+ if (exps.length > 1) {
132
+ // Check if outcomes differ, or if lessons are different
133
+ const outcomes = new Set(exps.map(e => e.outcome));
134
+ const lessons = new Set(exps.map(e => e.lesson));
135
+ if (outcomes.size > 1 || lessons.size > 1) {
136
+ conflicting.push(...exps);
137
+ }
138
+ }
139
+ }
140
+ return [...new Set(conflicting)];
141
+ }
142
+ export function validateExperience(ecosystemRoot, id, validationEvidence) {
143
+ const list = loadExperience(ecosystemRoot);
144
+ const exp = list.find(e => e.id === id);
145
+ if (!exp)
146
+ throw new Error("Experience not found.");
147
+ // In a real system we would verify the evidence links to real test results or requirements
148
+ exp.status = "VALIDATED";
149
+ exp.evidence = validationEvidence;
150
+ exp.updated_at = new Date().toISOString();
151
+ saveExperience(ecosystemRoot, list);
152
+ return exp;
153
+ }
154
+ export function getExperienceContext(ecosystemRoot, id) {
155
+ const list = loadExperience(ecosystemRoot);
156
+ const exp = list.find(e => e.id === id);
157
+ if (!exp)
158
+ throw new Error("Experience not found.");
159
+ return exp;
160
+ }
161
+ export function findRepeatedOutcomes(ecosystemRoot) {
162
+ const list = loadExperience(ecosystemRoot);
163
+ const outcomeMap = new Map();
164
+ for (const exp of list) {
165
+ if (exp.status === "VALIDATED" && exp.outcome) {
166
+ if (!outcomeMap.has(exp.outcome))
167
+ outcomeMap.set(exp.outcome, []);
168
+ outcomeMap.get(exp.outcome).push(exp);
169
+ }
170
+ }
171
+ const repeated = [];
172
+ for (const [outcome, exps] of outcomeMap.entries()) {
173
+ if (exps.length > 1 && outcome !== "UNKNOWN" && outcome !== "SUCCESS") {
174
+ repeated.push({ outcome, occurrences: exps });
175
+ }
176
+ }
177
+ return repeated;
178
+ }
179
+ export function markExperienceStale(ecosystemRoot, id, reason) {
180
+ const list = loadExperience(ecosystemRoot);
181
+ const exp = list.find(e => e.id === id);
182
+ if (!exp)
183
+ throw new Error("Experience not found.");
184
+ exp.status = "STALE";
185
+ exp.context = exp.context + `\nStale Reason: ${reason}`;
186
+ exp.updated_at = new Date().toISOString();
187
+ saveExperience(ecosystemRoot, list);
188
+ return exp;
189
+ }
190
+ export function retireExperience(ecosystemRoot, id, reason) {
191
+ const list = loadExperience(ecosystemRoot);
192
+ const exp = list.find(e => e.id === id);
193
+ if (!exp)
194
+ throw new Error("Experience not found.");
195
+ exp.status = "RETIRED";
196
+ exp.context = exp.context + `\nRetirement Reason: ${reason}`;
197
+ exp.updated_at = new Date().toISOString();
198
+ saveExperience(ecosystemRoot, list);
199
+ return exp;
200
+ }
201
+ export function linkExperience(ecosystemRoot, id, links) {
202
+ const list = loadExperience(ecosystemRoot);
203
+ const exp = list.find(e => e.id === id);
204
+ if (!exp)
205
+ throw new Error("Experience not found.");
206
+ if (links.source_requirement_id)
207
+ exp.source_requirement_id = links.source_requirement_id;
208
+ if (links.source_plan_id)
209
+ exp.source_plan_id = links.source_plan_id;
210
+ if (links.source_validation_id)
211
+ exp.source_validation_id = links.source_validation_id;
212
+ if (links.source_test_result_ids) {
213
+ exp.source_test_result_ids = [...new Set([...(exp.source_test_result_ids || []), ...links.source_test_result_ids])];
214
+ }
215
+ if (links.source_traceability_ids) {
216
+ exp.source_traceability_ids = [...new Set([...(exp.source_traceability_ids || []), ...links.source_traceability_ids])];
217
+ }
218
+ exp.updated_at = new Date().toISOString();
219
+ saveExperience(ecosystemRoot, list);
220
+ return exp;
221
+ }
222
+ export function getLearningFeedback(ecosystemRoot, requirementId, planId) {
223
+ const list = loadExperience(ecosystemRoot);
224
+ return list.filter(exp => (exp.source_requirement_id === requirementId) ||
225
+ (planId && exp.source_plan_id === planId));
226
+ }
@@ -0,0 +1,148 @@
1
+ export function buildGraph(dna, spec) {
2
+ const nodes = new Map();
3
+ const edges = new Map();
4
+ function addNode(node) {
5
+ if (!nodes.has(node.id)) {
6
+ nodes.set(node.id, node);
7
+ }
8
+ }
9
+ function addEdge(edge) {
10
+ if (!edges.has(edge.id)) {
11
+ edges.set(edge.id, edge);
12
+ }
13
+ }
14
+ // 1. Spec Services
15
+ if (spec && spec.microservices) {
16
+ for (const [svcName, svcData] of Object.entries(spec.microservices)) {
17
+ const svcId = `SERVICE:${svcName}`;
18
+ addNode({ id: svcId, type: "SERVICE", name: svcName, metadata: svcData });
19
+ // Calls Services
20
+ if (Array.isArray(svcData.calls_services)) {
21
+ for (const targetSvc of svcData.calls_services) {
22
+ const targetId = `SERVICE:${targetSvc}`;
23
+ addNode({ id: targetId, type: "SERVICE", name: targetSvc });
24
+ addEdge({ id: `${svcId}-CALLS-${targetId}`, source: svcId, target: targetId, type: "CALLS" });
25
+ }
26
+ }
27
+ // Owns Tables
28
+ if (Array.isArray(svcData.owns_tables)) {
29
+ for (const table of svcData.owns_tables) {
30
+ const tableId = `DATABASE_TABLE:${table}`;
31
+ addNode({ id: tableId, type: "DATABASE_TABLE", name: table });
32
+ addEdge({ id: `${svcId}-OWNS-${tableId}`, source: svcId, target: tableId, type: "OWNS" });
33
+ }
34
+ }
35
+ // Kafka Consumes
36
+ if (Array.isArray(svcData.kafka_consumes)) {
37
+ for (const topic of svcData.kafka_consumes) {
38
+ const topicId = `TOPIC:${topic}`;
39
+ addNode({ id: topicId, type: "TOPIC", name: topic });
40
+ addEdge({ id: `${svcId}-CONSUMES-${topicId}`, source: svcId, target: topicId, type: "CONSUMES" });
41
+ }
42
+ }
43
+ }
44
+ }
45
+ // 2. Parsed Domains
46
+ if (Array.isArray(dna.domains)) {
47
+ for (const domain of dna.domains) {
48
+ const classId = `CLASS:${domain.name}`;
49
+ const svcId = `SERVICE:${domain.repository}`;
50
+ addNode({
51
+ id: classId,
52
+ type: "CLASS",
53
+ name: domain.name,
54
+ repository: domain.repository,
55
+ metadata: { type: domain.type, violations: domain.violations },
56
+ evidence: { repository: domain.repository, filePath: domain.filePath }
57
+ });
58
+ addNode({ id: svcId, type: "SERVICE", name: domain.repository });
59
+ addEdge({ id: `${svcId}-CONTAINS-${classId}`, source: svcId, target: classId, type: "CONTAINS", evidence: { repository: domain.repository, filePath: domain.filePath } });
60
+ }
61
+ }
62
+ // 3. Parsed Events
63
+ if (Array.isArray(dna.events)) {
64
+ for (const event of dna.events) {
65
+ const classId = `CLASS:${event.className}`;
66
+ const svcId = `SERVICE:${event.repository}`;
67
+ addNode({
68
+ id: classId,
69
+ type: "CLASS",
70
+ name: event.className,
71
+ repository: event.repository,
72
+ evidence: { repository: event.repository, filePath: event.filePath }
73
+ });
74
+ if (Array.isArray(event.topicsConsumed)) {
75
+ for (const topic of event.topicsConsumed) {
76
+ const topicId = `TOPIC:${topic}`;
77
+ addNode({ id: topicId, type: "TOPIC", name: topic });
78
+ addEdge({ id: `${classId}-CONSUMES-${topicId}`, source: classId, target: topicId, type: "CONSUMES", evidence: { repository: event.repository, filePath: event.filePath } });
79
+ }
80
+ }
81
+ if (Array.isArray(event.topicsProduced)) {
82
+ for (const topic of event.topicsProduced) {
83
+ const topicId = `TOPIC:${topic}`;
84
+ addNode({ id: topicId, type: "TOPIC", name: topic });
85
+ addEdge({ id: `${classId}-PRODUCES-${topicId}`, source: classId, target: topicId, type: "PRODUCES", evidence: { repository: event.repository, filePath: event.filePath } });
86
+ }
87
+ }
88
+ }
89
+ }
90
+ // 4. Parsed APIs
91
+ if (Array.isArray(dna.apis)) {
92
+ for (const api of dna.apis) {
93
+ const apiId = `API:${api.controllerName}:${api.method}:${api.path}`;
94
+ const svcId = `SERVICE:${api.repository}`;
95
+ addNode({
96
+ id: apiId,
97
+ type: "API",
98
+ name: `${api.method} ${api.path}`,
99
+ repository: api.repository,
100
+ metadata: { controller: api.controllerName },
101
+ evidence: { repository: api.repository, filePath: api.filePath }
102
+ });
103
+ addEdge({ id: `${svcId}-EXPOSES-${apiId}`, source: svcId, target: apiId, type: "EXPOSES", evidence: { repository: api.repository, filePath: api.filePath } });
104
+ }
105
+ }
106
+ // 5. Parsed Databases
107
+ if (Array.isArray(dna.databases)) {
108
+ for (const db of dna.databases) {
109
+ const tableId = `DATABASE_TABLE:${db.table}`;
110
+ addNode({
111
+ id: tableId,
112
+ type: "DATABASE_TABLE",
113
+ name: db.table,
114
+ repository: db.repository,
115
+ evidence: { repository: db.repository, filePath: db.filePath }
116
+ });
117
+ }
118
+ }
119
+ // 6. Parsed Frontend
120
+ if (Array.isArray(dna.frontend)) {
121
+ for (const fe of dna.frontend) {
122
+ const compId = `FRONTEND_COMPONENT:${fe.componentName || fe.name}`;
123
+ const svcId = `FRONTEND_APP:${fe.repository}`;
124
+ addNode({ id: svcId, type: "FRONTEND_APP", name: fe.repository });
125
+ addNode({
126
+ id: compId,
127
+ type: fe.isPattern ? "UI_PATTERN" : "FRONTEND_COMPONENT",
128
+ name: fe.componentName || fe.name,
129
+ repository: fe.repository,
130
+ metadata: { type: fe.type, ...fe },
131
+ evidence: { repository: fe.repository, filePath: fe.filePath }
132
+ });
133
+ addEdge({ id: `${svcId}-CONTAINS-${compId}`, source: svcId, target: compId, type: "CONTAINS" });
134
+ if (fe.routes_to) {
135
+ for (const route of fe.routes_to) {
136
+ const routeId = `FRONTEND_ROUTE:${route}`;
137
+ addNode({ id: routeId, type: "FRONTEND_ROUTE", name: route });
138
+ addEdge({ id: `${compId}-ROUTES_TO-${routeId}`, source: compId, target: routeId, type: "ROUTES_TO" });
139
+ }
140
+ }
141
+ }
142
+ }
143
+ return {
144
+ schema_version: "1.0",
145
+ nodes: Array.from(nodes.values()),
146
+ edges: Array.from(edges.values())
147
+ };
148
+ }
@@ -0,0 +1,92 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ export function loadPlan(ecosystemRoot, id) {
4
+ const p = path.join(ecosystemRoot, '.secufusion', 'plans', id + '.json');
5
+ if (!fs.existsSync(p))
6
+ return null;
7
+ try {
8
+ return JSON.parse(fs.readFileSync(p, 'utf-8'));
9
+ }
10
+ catch {
11
+ return null;
12
+ }
13
+ }
14
+ export function savePlan(ecosystemRoot, plan) {
15
+ const dir = path.join(ecosystemRoot, '.secufusion', 'plans');
16
+ if (!fs.existsSync(dir))
17
+ fs.mkdirSync(dir, { recursive: true });
18
+ plan.updated_at = new Date().toISOString();
19
+ if (!plan.created_at)
20
+ plan.created_at = plan.updated_at;
21
+ fs.writeFileSync(path.join(dir, plan.id + '.json'), JSON.stringify(plan, null, 2));
22
+ }
23
+ // Destructive operation keywords
24
+ const DESTRUCTIVE_KEYWORDS = ["DELETE", "DROP", "PURGE", "REMOVE", "REVOKE", "DISABLE", "HARD_DELETE", "MASS_DELETE", "anonymize", "cleanup", "invalidation"];
25
+ export function analyzeDestructiveOperations(plan) {
26
+ const destructive = [];
27
+ const allChanges = [
28
+ ...plan.api_changes,
29
+ ...plan.database_changes,
30
+ ...plan.event_changes,
31
+ ...plan.implementation_steps
32
+ ].join(" ").toLowerCase();
33
+ for (const keyword of DESTRUCTIVE_KEYWORDS) {
34
+ if (allChanges.includes(keyword.toLowerCase())) {
35
+ destructive.push(keyword);
36
+ }
37
+ }
38
+ return destructive;
39
+ }
40
+ export function validatePlan(ecosystemRoot, plan) {
41
+ const errors = [];
42
+ // 1. Check Requirement exists and is CONFIRMED
43
+ const reqPath = path.join(ecosystemRoot, '.secufusion', 'requirements', plan.requirement_id + '.json');
44
+ if (!fs.existsSync(reqPath)) {
45
+ errors.push("REQUIREMENT_NOT_FOUND");
46
+ }
47
+ else {
48
+ const req = JSON.parse(fs.readFileSync(reqPath, "utf-8"));
49
+ if (req.status !== "CONFIRMED") {
50
+ errors.push("REQUIREMENT_NOT_CONFIRMED (" + req.status + ")");
51
+ }
52
+ }
53
+ // 2. Check Stale Evidence
54
+ // In a real system, we'd check if the snapshot_id matches current git commit or extraction timestamp
55
+ const dnaPath = path.join(ecosystemRoot, ".secufusion", "dna.json");
56
+ if (fs.existsSync(dnaPath)) {
57
+ const stats = fs.statSync(dnaPath);
58
+ // Simplified stale check: if plan is older than last extraction, it might be stale.
59
+ if (new Date(plan.created_at).getTime() < stats.mtime.getTime()) {
60
+ errors.push("STALE_EVIDENCE");
61
+ }
62
+ }
63
+ // 3. Destructive Operations Guardrails
64
+ const destructive = analyzeDestructiveOperations(plan);
65
+ if (destructive.length > 0) {
66
+ // If destructive operations exist, ensure they are explicitly confirmed in the requirement decisions
67
+ let decisionFound = false;
68
+ if (fs.existsSync(reqPath)) {
69
+ const req = JSON.parse(fs.readFileSync(reqPath, "utf-8"));
70
+ const decisionsStr = (req.decisions || []).join(" ").toLowerCase();
71
+ if (destructive.some(d => decisionsStr.includes(d.toLowerCase()))) {
72
+ decisionFound = true;
73
+ }
74
+ }
75
+ if (!decisionFound) {
76
+ errors.push("USER_DECISION_REQUIRED: Destructive operations detected (" + destructive.join(", ") + ") without explicit requirement decision.");
77
+ }
78
+ }
79
+ // 4. Traceability & Tests
80
+ if (!plan.test_obligations || plan.test_obligations.length === 0) {
81
+ errors.push("MISSING_TEST_OBLIGATIONS");
82
+ }
83
+ // 5. Blockers & Unknowns
84
+ if (plan.blockers && plan.blockers.length > 0) {
85
+ errors.push("BLOCKED");
86
+ }
87
+ if (plan.unknowns && plan.unknowns.length > 0) {
88
+ // Implementation-critical unknowns shouldn't exist
89
+ errors.push("UNRESOLVED_UNKNOWNS");
90
+ }
91
+ return errors;
92
+ }
@@ -0,0 +1,59 @@
1
+ export function investigateEntity(graph, entityName) {
2
+ const targetLower = entityName.toLowerCase();
3
+ const nodes = graph.nodes.filter(n => n.id.toLowerCase().includes(targetLower) || n.name.toLowerCase().includes(targetLower));
4
+ if (nodes.length === 0) {
5
+ return {
6
+ state: "NOT_FOUND",
7
+ evidence: [],
8
+ nodes_found: [],
9
+ edges_found: [],
10
+ summary: `No nodes matching '${entityName}' were found in the current evidence graph.`
11
+ };
12
+ }
13
+ const edges = graph.edges.filter(e => nodes.some(n => n.id === e.source || n.id === e.target));
14
+ const evidenceSet = new Set();
15
+ nodes.forEach(n => n.evidence?.filePath && evidenceSet.add(n.evidence.filePath));
16
+ edges.forEach(e => e.evidence?.filePath && evidenceSet.add(e.evidence.filePath));
17
+ return {
18
+ state: "FOUND",
19
+ evidence: Array.from(evidenceSet),
20
+ nodes_found: nodes,
21
+ edges_found: edges,
22
+ summary: `Found ${nodes.length} nodes and ${edges.length} relationships for '${entityName}'.`
23
+ };
24
+ }
25
+ export function investigateRelationship(graph, sourceEntity, targetEntity) {
26
+ const sourceLower = sourceEntity.toLowerCase();
27
+ const targetLower = targetEntity.toLowerCase();
28
+ const sourceNodes = graph.nodes.filter(n => n.id.toLowerCase().includes(sourceLower) || n.name.toLowerCase().includes(sourceLower));
29
+ const targetNodes = graph.nodes.filter(n => n.id.toLowerCase().includes(targetLower) || n.name.toLowerCase().includes(targetLower));
30
+ if (sourceNodes.length === 0 || targetNodes.length === 0) {
31
+ return {
32
+ state: "NOT_FOUND",
33
+ evidence: [],
34
+ nodes_found: [],
35
+ edges_found: [],
36
+ summary: `Could not establish relationship because one or both entities were not found.`
37
+ };
38
+ }
39
+ const edges = graph.edges.filter(e => sourceNodes.some(s => s.id === e.source) && targetNodes.some(t => t.id === e.target) ||
40
+ sourceNodes.some(s => s.id === e.target) && targetNodes.some(t => t.id === e.source));
41
+ if (edges.length === 0) {
42
+ return {
43
+ state: "NOT_FOUND",
44
+ evidence: [],
45
+ nodes_found: [...sourceNodes, ...targetNodes],
46
+ edges_found: [],
47
+ summary: `Both entities exist, but no direct relationship was found between them in the evidence graph.`
48
+ };
49
+ }
50
+ const evidenceSet = new Set();
51
+ edges.forEach(e => e.evidence?.filePath && evidenceSet.add(e.evidence.filePath));
52
+ return {
53
+ state: "FOUND",
54
+ evidence: Array.from(evidenceSet),
55
+ nodes_found: [...sourceNodes, ...targetNodes],
56
+ edges_found: edges,
57
+ summary: `Found ${edges.length} direct relationships between '${sourceEntity}' and '${targetEntity}'.`
58
+ };
59
+ }