a06f331eb8
CI / benchmark (push) Has been skipped
install-script / posix-syntax (push) Successful in 6m1s
CI / build-onnx (push) Failing after 6m43s
init-smoke / dry-run (push) Failing after 15m57s
security / govulncheck (push) Has been cancelled
security / trivy-fs (push) Has been cancelled
CI / test (1.26, ubuntu-latest) (push) Has been cancelled
Scorecard supply-chain security / Scorecard analysis (push) Has been cancelled
CI / test (1.26, macos-latest) (push) Has been cancelled
CI / build-windows (push) Has been cancelled
CI / lint (push) Has been cancelled
install-script / powershell-syntax (push) Has been cancelled
install-script / install (macos-14) (push) Has been cancelled
install-script / install (ubuntu-latest) (push) Has been cancelled
137 lines
4.4 KiB
Go
137 lines
4.4 KiB
Go
package resolver
|
|
|
|
import (
|
|
"strings"
|
|
|
|
"github.com/zzet/gortex/internal/graph"
|
|
)
|
|
|
|
// TemporalOrphan names one side of a Temporal contract that has no
|
|
// counterpart in the graph.
|
|
type TemporalOrphan struct {
|
|
From string `json:"from"` // the dispatching / sending node
|
|
Kind string `json:"kind"` // activity / workflow / signal / query
|
|
Name string `json:"name"` // the dispatched / signalled / queried name
|
|
File string `json:"file,omitempty"` // call-site file, when known
|
|
Line int `json:"line,omitempty"`
|
|
}
|
|
|
|
// TemporalOrphanReport is the result of DetectTemporalOrphans. Each list
|
|
// is a different integrity gap in the Temporal call graph.
|
|
type TemporalOrphanReport struct {
|
|
// BrokenDispatch: a workflow dispatches an activity / child workflow
|
|
// whose name resolves to nothing (the temporal.stub edge is still a
|
|
// placeholder). Almost always an error — a broken or renamed call.
|
|
BrokenDispatch []TemporalOrphan `json:"broken_dispatch"`
|
|
// SignalNoHandler / QueryNoHandler: a signal is sent / a query is
|
|
// called with a name no workflow handles. An error (sending into the
|
|
// void).
|
|
SignalNoHandler []TemporalOrphan `json:"signal_no_handler"`
|
|
QueryNoHandler []TemporalOrphan `json:"query_no_handler"`
|
|
// OrphanActivity / OrphanWorkflow: a registered activity / workflow
|
|
// nobody dispatches or starts. A warning — dead code or unfinished.
|
|
OrphanActivity []string `json:"orphan_activity"`
|
|
OrphanWorkflow []string `json:"orphan_workflow"`
|
|
}
|
|
|
|
// DetectTemporalOrphans walks the resolved Temporal graph and reports the
|
|
// four integrity gaps above. Read-only.
|
|
func DetectTemporalOrphans(g graph.Store) TemporalOrphanReport {
|
|
var rep TemporalOrphanReport
|
|
if g == nil {
|
|
return rep
|
|
}
|
|
|
|
// Signal/query handler name sets (providers).
|
|
signalHandled := map[string]bool{}
|
|
queryHandled := map[string]bool{}
|
|
// Activities/workflows that have at least one resolved inbound
|
|
// dispatch (consumed).
|
|
consumed := map[string]bool{}
|
|
|
|
for e := range g.EdgesByKind(graph.EdgeCalls) {
|
|
if e == nil || e.Meta == nil {
|
|
continue
|
|
}
|
|
via, _ := e.Meta["via"].(string)
|
|
kind, _ := e.Meta["temporal_kind"].(string)
|
|
name, _ := e.Meta["temporal_name"].(string)
|
|
switch via {
|
|
case "temporal.handler":
|
|
switch kind {
|
|
case "signal":
|
|
signalHandled[name] = true
|
|
case "query":
|
|
queryHandled[name] = true
|
|
}
|
|
case "temporal.stub":
|
|
if strings.HasPrefix(e.To, temporalStubPrefix) {
|
|
// P0: a dispatch whose call site is a test file is almost
|
|
// always a fixture/mock (handler is a stub or lives in
|
|
// another repo); counting it as a broken_dispatch is the
|
|
// dominant false positive. Skip it — keyed on the edge's
|
|
// own FilePath (the dispatcher), so it's robust under both
|
|
// full and incremental reindex (no Node.Meta dependency).
|
|
if isTestFilePath(e.FilePath) {
|
|
continue
|
|
}
|
|
rep.BrokenDispatch = append(rep.BrokenDispatch, TemporalOrphan{
|
|
From: e.From, Kind: kind, Name: name, File: e.FilePath, Line: e.Line,
|
|
})
|
|
} else if e.To != "" {
|
|
consumed[e.To] = true
|
|
}
|
|
}
|
|
}
|
|
|
|
// Second pass for senders/callers now that the handler sets are known.
|
|
for e := range g.EdgesByKind(graph.EdgeCalls) {
|
|
if e == nil || e.Meta == nil {
|
|
continue
|
|
}
|
|
via, _ := e.Meta["via"].(string)
|
|
name, _ := e.Meta["temporal_name"].(string)
|
|
switch via {
|
|
case "temporal.signal-send":
|
|
if name != "" && !signalHandled[name] {
|
|
rep.SignalNoHandler = append(rep.SignalNoHandler, TemporalOrphan{
|
|
From: e.From, Kind: "signal", Name: name, File: e.FilePath, Line: e.Line,
|
|
})
|
|
}
|
|
case "temporal.query-call":
|
|
if name != "" && !queryHandled[name] {
|
|
rep.QueryNoHandler = append(rep.QueryNoHandler, TemporalOrphan{
|
|
From: e.From, Kind: "query", Name: name, File: e.FilePath, Line: e.Line,
|
|
})
|
|
}
|
|
}
|
|
}
|
|
|
|
// Registered-but-unconsumed activities / workflows. Only Go nodes
|
|
// carry temporal_role from a worker.Register* call; an activity with
|
|
// no resolved inbound dispatch is dead.
|
|
checkOrphanRole := func(n *graph.Node) {
|
|
if n == nil || n.Language != "go" {
|
|
return
|
|
}
|
|
role, _ := n.Meta["temporal_role"].(string)
|
|
switch role {
|
|
case "activity":
|
|
if !consumed[n.ID] {
|
|
rep.OrphanActivity = append(rep.OrphanActivity, n.ID)
|
|
}
|
|
case "workflow":
|
|
if !consumed[n.ID] {
|
|
rep.OrphanWorkflow = append(rep.OrphanWorkflow, n.ID)
|
|
}
|
|
}
|
|
}
|
|
for n := range g.NodesByKind(graph.KindFunction) {
|
|
checkOrphanRole(n)
|
|
}
|
|
for n := range g.NodesByKind(graph.KindMethod) {
|
|
checkOrphanRole(n)
|
|
}
|
|
return rep
|
|
}
|