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