package knowledge // RescoreGraphIntentFit recomputes node productFitScore using: // 1. weighted intent similarity (product + research map) // 2. graph proximity propagation from high-fit core nodes // 3. tangential-topic penalty func RescoreGraphIntentFit(graph *Graph, in PatrolTagInput) { if graph == nil || len(graph.Nodes) == 0 { return } profile := BuildIntentProfile(in) intentScores := make([]int, len(graph.Nodes)) for i, node := range graph.Nodes { intentScores[i] = ScoreIntentSimilarity(NodeIntentText(node), profile) } propagated := propagateIntentAlongGraph(graph.Nodes, graph.Edges, intentScores) for i := range graph.Nodes { node := &graph.Nodes[i] semantic := intentScores[i] if propagated[i] > semantic { semantic = propagated[i] } llmScore := node.ProductFitScore if llmScore <= 0 { llmScore = defaultProductFit(node.NodeKind, node.Layer) } layerBlend := 0.5 + float64(minInt(node.Layer, 3))*0.08 blended := int(float64(semantic)*layerBlend + float64(llmScore)*(1.0-layerBlend)) if IsTangentialToIntent(NodeIntentText(*node), profile) { blended = minInt(blended, 28) } if blended < 0 { blended = 0 } if blended > 100 { blended = 100 } node.ProductFitScore = blended } } func propagateIntentAlongGraph(nodes []Node, edges []Edge, intentScores []int) []int { if len(nodes) == 0 { return nil } idIndex := map[string]int{} for i, node := range nodes { idIndex[node.ID] = i } adj := make([][]int, len(nodes)) addEdge := func(from, to string) { fi, okFrom := idIndex[from] ti, okTo := idIndex[to] if !okFrom || !okTo || fi == ti { return } adj[fi] = append(adj[fi], ti) adj[ti] = append(adj[ti], fi) } for _, edge := range edges { addEdge(edge.From, edge.To) } out := make([]int, len(nodes)) copy(out, intentScores) visited := make([]bool, len(nodes)) type queueItem struct { idx int dist int } queue := make([]queueItem, 0, len(nodes)) for i, score := range intentScores { if score >= 55 || nodes[i].Layer == 0 { queue = append(queue, queueItem{idx: i, dist: 0}) visited[i] = true } } for len(queue) > 0 { item := queue[0] queue = queue[1:] if item.dist >= 3 { continue } base := intentScores[item.idx] if base <= 0 { base = out[item.idx] } decayed := int(float64(base) * powDecay(0.74, item.dist+1)) for _, next := range adj[item.idx] { if decayed > out[next] { out[next] = decayed } if !visited[next] { visited[next] = true queue = append(queue, queueItem{idx: next, dist: item.dist + 1}) } } } return out } func powDecay(base float64, exp int) float64 { out := 1.0 for i := 0; i < exp; i++ { out *= base } return out }