turash/bugulma/backend/internal/graph/network_analyzer.go
Damir Mukimov 000eab4740
Major repository reorganization and missing backend endpoints implementation
Repository Structure:
- Move files from cluttered root directory into organized structure
- Create archive/ for archived data and scraper results
- Create bugulma/ for the complete application (frontend + backend)
- Create data/ for sample datasets and reference materials
- Create docs/ for comprehensive documentation structure
- Create scripts/ for utility scripts and API tools

Backend Implementation:
- Implement 3 missing backend endpoints identified in gap analysis:
  * GET /api/v1/organizations/{id}/matching/direct - Direct symbiosis matches
  * GET /api/v1/users/me/organizations - User organizations
  * POST /api/v1/proposals/{id}/status - Update proposal status
- Add complete proposal domain model, repository, and service layers
- Create database migration for proposals table
- Fix CLI server command registration issue

API Documentation:
- Add comprehensive proposals.md API documentation
- Update README.md with Users and Proposals API sections
- Document all request/response formats, error codes, and business rules

Code Quality:
- Follow existing Go backend architecture patterns
- Add proper error handling and validation
- Match frontend expected response schemas
- Maintain clean separation of concerns (handler -> service -> repository)
2025-11-25 06:01:16 +01:00

207 lines
5.7 KiB
Go

package graph
import (
"context"
"fmt"
"github.com/neo4j/neo4j-go-driver/v5/neo4j"
)
// NetworkAnalyzer handles network analysis operations
type NetworkAnalyzer struct {
sessionManager *SessionManager
queryTemplates *QueryTemplates
config *Config
}
// NewNetworkAnalyzer creates a new network analyzer instance
func NewNetworkAnalyzer(sessionManager *SessionManager, queryTemplates *QueryTemplates, config *Config) *NetworkAnalyzer {
return &NetworkAnalyzer{
sessionManager: sessionManager,
queryTemplates: queryTemplates,
config: config,
}
}
// AnalyzeNetworkCentrality calculates centrality measures for organizations
func (na *NetworkAnalyzer) AnalyzeNetworkCentrality(ctx context.Context) (map[string]*CentralityMetrics, error) {
result, err := na.sessionManager.ExecuteRead(ctx, func(tx neo4j.ManagedTransaction) (interface{}, error) {
cypher := na.queryTemplates.CalculateCentralityQuery()
queryResult, err := tx.Run(ctx, cypher, nil)
if err != nil {
return nil, fmt.Errorf("failed to execute centrality query: %w", err)
}
centralityMap := make(map[string]*CentralityMetrics)
totalOrgs := 0.0
maxDegree := 0.0
// First pass: collect degree centrality
for queryResult.Next(ctx) {
record := queryResult.Record()
orgID, _ := record.Get("org_id")
degree, _ := record.Get("degree_centrality")
outFlows, _ := record.Get("out_flows")
inFlows, _ := record.Get("in_flows")
orgIDStr := getStringValue(orgID)
degreeVal := getFloat64Value(degree)
if degreeVal > maxDegree {
maxDegree = degreeVal
}
centralityMap[orgIDStr] = &CentralityMetrics{
OrganizationID: orgIDStr,
Degree: degreeVal,
Betweenness: 0.0, // Would need more complex calculation
Closeness: 0.0, // Would need path analysis
OutFlows: getIntValue(outFlows),
InFlows: getIntValue(inFlows),
}
totalOrgs++
}
if err := queryResult.Err(); err != nil {
return nil, fmt.Errorf("failed to iterate centrality results: %w", err)
}
// Normalize degree centrality (0-1 scale)
if maxDegree > 0 {
for _, metrics := range centralityMap {
metrics.Degree = metrics.Degree / maxDegree
}
}
return centralityMap, nil
})
if err != nil {
return nil, err
}
return result.(map[string]*CentralityMetrics), nil
}
// GetNetworkStatistics provides overall network statistics
func (na *NetworkAnalyzer) GetNetworkStatistics(ctx context.Context) (*NetworkStatistics, error) {
result, err := na.sessionManager.ExecuteRead(ctx, func(tx neo4j.ManagedTransaction) (interface{}, error) {
cypher := na.queryTemplates.GetNetworkStatisticsQuery()
queryResult, err := tx.Run(ctx, cypher, nil)
if err != nil {
return nil, fmt.Errorf("failed to execute network statistics query: %w", err)
}
if queryResult.Next(ctx) {
record := queryResult.Record()
stats := &NetworkStatistics{}
if val, ok := record.Get("total_organizations"); ok {
stats.TotalOrganizations = getIntValue(val)
}
if val, ok := record.Get("total_resource_flows"); ok {
stats.TotalResourceFlows = getIntValue(val)
}
if val, ok := record.Get("total_matches"); ok {
stats.TotalMatches = getIntValue(val)
}
if val, ok := record.Get("total_economic_value"); ok {
stats.TotalEconomicValue = getFloat64Value(val)
}
if val, ok := record.Get("avg_distance"); ok {
stats.AverageDistance = getFloat64Value(val)
}
if val, ok := record.Get("high_value_matches"); ok {
stats.HighValueMatches = getIntValue(val)
}
if val, ok := record.Get("geographic_span_km"); ok {
stats.GeographicSpanKm = getFloat64Value(val)
}
return stats, nil
}
// Return empty stats if no results
return &NetworkStatistics{}, nil
})
if err != nil {
return nil, err
}
return result.(*NetworkStatistics), nil
}
// FindCircularEconomyCycles finds circular resource flow cycles
func (na *NetworkAnalyzer) FindCircularEconomyCycles(ctx context.Context) ([]*CircularEconomyCycle, error) {
result, err := na.sessionManager.ExecuteRead(ctx, func(tx neo4j.ManagedTransaction) (interface{}, error) {
cypher := na.queryTemplates.FindCircularEconomyCyclesQuery()
queryResult, err := tx.Run(ctx, cypher, map[string]interface{}{
"limit": na.config.DefaultResultLimit,
})
if err != nil {
return nil, fmt.Errorf("failed to execute circular economy query: %w", err)
}
var cycles []*CircularEconomyCycle
for queryResult.Next(ctx) {
record := queryResult.Record()
cycleLength, _ := record.Get("cycle_length")
flowIDs, _ := record.Get("flow_ids")
organizations, _ := record.Get("organizations")
cycleValue, _ := record.Get("cycle_value")
orgCount, _ := record.Get("org_count")
cycle := &CircularEconomyCycle{
CycleID: fmt.Sprintf("cycle_%d", len(cycles)+1),
CycleLength: getIntValue(cycleLength),
CycleValue: getFloat64Value(cycleValue),
OrgCount: getIntValue(orgCount),
FlowIDs: []string{},
Organizations: []string{},
}
// Convert flow IDs
if flowIDsSlice, ok := flowIDs.([]interface{}); ok {
for _, flowID := range flowIDsSlice {
if flowIDStr, ok := flowID.(string); ok {
cycle.FlowIDs = append(cycle.FlowIDs, flowIDStr)
}
}
}
// Convert organizations
if orgsSlice, ok := organizations.([]interface{}); ok {
for _, org := range orgsSlice {
if orgStr, ok := org.(string); ok {
cycle.Organizations = append(cycle.Organizations, orgStr)
}
}
}
cycles = append(cycles, cycle)
}
if err := queryResult.Err(); err != nil {
return nil, fmt.Errorf("failed to iterate cycle results: %w", err)
}
return cycles, nil
})
if err != nil {
return nil, err
}
return result.([]*CircularEconomyCycle), nil
}