返回 DeepSeek-Reasonix
inspect.go
根目录 / internal / skill / inspect.go
1 package skill
2
3 import (
4 "context"
5 "io"
6 "sort"
7 "strings"
8 )
9
10 // CandidateStatus is the diagnostic disposition of one skill candidate.
11 type CandidateStatus string
12
13 const (
14 CandidateWinner CandidateStatus = "winner"
15 CandidateShadowed CandidateStatus = "shadowed"
16 CandidateDisabled CandidateStatus = "disabled"
17 )
18
19 // Candidate is one discovered skill entry, including shadowed and disabled ones
20 // that Store.List omits. Used by capability diagnostics so rules stay aligned
21 // with discovery without changing List/Read behavior.
22 type Candidate struct {
23 Name string
24 Description string
25 Scope Scope
26 Path string
27 Status CandidateStatus
28 WinnerPath string // set when Status is shadowed
29 RunAs RunAs
30 }
31
32 // Inspection is a read-only snapshot of skill discovery for diagnostics.
33 type Inspection struct {
34 Roots []Root
35 Candidates []Candidate
36 }
37
38 // Inspect walks the same roots as List but keeps shadowed and disabled
39 // candidates. Missing convention roots stay StatusMissing without issues.
40 // Stderr parse warnings are suppressed so diagnostics stay quiet.
41 func (s *Store) Inspect() Inspection {
42 if s == nil || s.disableDiscovery {
43 return Inspection{}
44 }
45 origStderr := s.stderr
46 s.stderr = io.Discard
47 defer func() { s.stderr = origStderr }()
48
49 roots := s.Roots()
50 var candidates []Candidate
51 winnerByName := map[string]Candidate{}
52
53 // Scan roots highest-priority first (same as List).
54 for _, r := range s.roots() {
55 if r.Status != StatusOK {
56 continue
57 }
58 for _, sk := range s.discoverRoot(context.Background(), r) {
59 candidates = append(candidates, classifyCandidate(sk, s.disabledName(sk.Name), winnerByName)...)
60 if !s.disabledName(sk.Name) {
61 if _, ok := winnerByName[sk.Name]; !ok {
62 winnerByName[sk.Name] = Candidate{
63 Name: sk.Name, Description: sk.Description, Scope: sk.Scope,
64 Path: sk.Path, Status: CandidateWinner, RunAs: sk.RunAs,
65 }
66 }
67 }
68 }
69 }
70
71 if !s.disableBuiltins {
72 for _, sk := range builtinSkills() {
73 candidates = append(candidates, classifyCandidate(sk, s.disabledName(sk.Name), winnerByName)...)
74 if !s.disabledName(sk.Name) {
75 if _, ok := winnerByName[sk.Name]; !ok {
76 winnerByName[sk.Name] = Candidate{
77 Name: sk.Name, Description: sk.Description, Scope: sk.Scope,
78 Path: sk.Path, Status: CandidateWinner, RunAs: sk.RunAs,
79 }
80 }
81 }
82 }
83 }
84
85 sort.SliceStable(candidates, func(i, j int) bool {
86 if candidates[i].Name != candidates[j].Name {
87 return candidates[i].Name < candidates[j].Name
88 }
89 order := map[CandidateStatus]int{
90 CandidateWinner: 0, CandidateDisabled: 1, CandidateShadowed: 2,
91 }
92 return order[candidates[i].Status] < order[candidates[j].Status]
93 })
94
95 return Inspection{Roots: roots, Candidates: candidates}
96 }
97
98 func classifyCandidate(sk Skill, disabled bool, winners map[string]Candidate) []Candidate {
99 if disabled {
100 return []Candidate{{
101 Name: sk.Name, Description: sk.Description, Scope: sk.Scope,
102 Path: sk.Path, Status: CandidateDisabled, RunAs: sk.RunAs,
103 }}
104 }
105 if win, ok := winners[sk.Name]; ok {
106 return []Candidate{{
107 Name: sk.Name, Description: sk.Description, Scope: sk.Scope,
108 Path: sk.Path, Status: CandidateShadowed, WinnerPath: win.Path, RunAs: sk.RunAs,
109 }}
110 }
111 return []Candidate{{
112 Name: sk.Name, Description: sk.Description, Scope: sk.Scope,
113 Path: sk.Path, Status: CandidateWinner, RunAs: sk.RunAs,
114 }}
115 }
116
117 // MissingDescription reports whether a winner skill lacks a usable description.
118 func MissingDescription(desc string) bool {
119 return strings.TrimSpace(desc) == ""
120 }
121
121 lines GO