mirror of
https://github.com/larksuite/cli.git
synced 2026-07-06 16:18:05 +08:00
150 lines
3.9 KiB
Go
150 lines
3.9 KiB
Go
// Copyright (c) 2026 Lark Technologies Pte. Ltd.
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
package rules
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"io/fs"
|
|
"path/filepath"
|
|
"sort"
|
|
"strings"
|
|
|
|
"github.com/larksuite/cli/internal/qualitygate/facts"
|
|
"github.com/larksuite/cli/internal/qualitygate/report"
|
|
"github.com/larksuite/cli/internal/vfs"
|
|
)
|
|
|
|
type SkillDoc struct {
|
|
File string
|
|
Name string
|
|
Description string
|
|
Body string
|
|
}
|
|
|
|
func LoadSkillDocs(skillsDir string) ([]SkillDoc, error) {
|
|
var out []SkillDoc
|
|
if err := walkSkillDocs(skillsDir, func(path string) error {
|
|
data, err := vfs.ReadFile(path)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
out = append(out, parseSkillDoc(path, string(data)))
|
|
return nil
|
|
}); err != nil {
|
|
if errors.Is(err, fs.ErrNotExist) {
|
|
return nil, nil
|
|
}
|
|
return nil, err
|
|
}
|
|
sort.Slice(out, func(i, j int) bool {
|
|
return out[i].File < out[j].File
|
|
})
|
|
return out, nil
|
|
}
|
|
|
|
func CheckSkillQuality(docs []SkillDoc) ([]report.Diagnostic, []facts.SkillQualityFact) {
|
|
var diags []report.Diagnostic
|
|
var out []facts.SkillQualityFact
|
|
for _, doc := range docs {
|
|
criticalCount := strings.Count(doc.Body, "CRITICAL")
|
|
wordCount := len(strings.Fields(doc.Body))
|
|
fact := facts.SkillQualityFact{
|
|
SourceFile: doc.File,
|
|
WordCount: wordCount,
|
|
CriticalCount: criticalCount,
|
|
DescriptionLength: len([]rune(doc.Description)),
|
|
CriticalOverBudget: criticalCount > 3,
|
|
}
|
|
if fact.CriticalOverBudget {
|
|
diags = append(diags, report.Diagnostic{
|
|
Rule: "skill_critical_noise",
|
|
Action: report.ActionWarning,
|
|
File: doc.File,
|
|
Message: fmt.Sprintf("skill has %d CRITICAL markers; keep hard instructions focused", criticalCount),
|
|
Suggestion: "reduce CRITICAL markers to at most 3 and move procedural detail into references",
|
|
})
|
|
}
|
|
if fact.DescriptionLength < 20 || fact.DescriptionLength > 500 {
|
|
diags = append(diags, report.Diagnostic{
|
|
Rule: "skill_description_route_quality",
|
|
Action: report.ActionWarning,
|
|
File: doc.File,
|
|
Message: fmt.Sprintf("description length is %d runes; routing description may be too vague or too noisy", fact.DescriptionLength),
|
|
Suggestion: "write a concise WHAT / WHEN / NOT description for skill routing",
|
|
})
|
|
}
|
|
if wordCount > 2500 {
|
|
diags = append(diags, report.Diagnostic{
|
|
Rule: "skill_size_budget",
|
|
Action: report.ActionWarning,
|
|
File: doc.File,
|
|
Message: fmt.Sprintf("skill body has %d words", wordCount),
|
|
Suggestion: "move long procedural sections into references and keep SKILL.md focused on routing",
|
|
})
|
|
}
|
|
out = append(out, fact)
|
|
}
|
|
return diags, out
|
|
}
|
|
|
|
func walkSkillDocs(root string, visit func(string) error) error {
|
|
entries, err := vfs.ReadDir(root)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
for _, entry := range entries {
|
|
path := filepath.Join(root, entry.Name())
|
|
if entry.IsDir() {
|
|
if err := walkSkillDocs(path, visit); err != nil {
|
|
return err
|
|
}
|
|
continue
|
|
}
|
|
if entry.Type()&fs.ModeType != 0 || entry.Name() != "SKILL.md" {
|
|
continue
|
|
}
|
|
if err := visit(path); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func parseSkillDoc(path, raw string) SkillDoc {
|
|
doc := SkillDoc{File: path, Body: raw}
|
|
lines := strings.Split(raw, "\n")
|
|
if len(lines) == 0 || strings.TrimSpace(lines[0]) != "---" {
|
|
return doc
|
|
}
|
|
|
|
end := -1
|
|
for i := 1; i < len(lines); i++ {
|
|
if strings.TrimSpace(lines[i]) == "---" {
|
|
end = i
|
|
break
|
|
}
|
|
key, value, ok := strings.Cut(lines[i], ":")
|
|
if !ok {
|
|
continue
|
|
}
|
|
switch strings.TrimSpace(key) {
|
|
case "name":
|
|
doc.Name = trimFrontmatterValue(value)
|
|
case "description":
|
|
doc.Description = trimFrontmatterValue(value)
|
|
}
|
|
}
|
|
if end >= 0 && end+1 < len(lines) {
|
|
doc.Body = strings.Join(lines[end+1:], "\n")
|
|
}
|
|
return doc
|
|
}
|
|
|
|
func trimFrontmatterValue(value string) string {
|
|
value = strings.TrimSpace(value)
|
|
value = strings.Trim(value, `"'`)
|
|
return value
|
|
}
|