{"record":{"id":"f26c3a748520fc03","repo":"larksuite/cli","slug":"duplicate-skill-s","errorCode":null,"errorMessage":"duplicate skill %s","messagePattern":"duplicate skill (.+?)","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"internal/skillscheck/sync.go","lineNumber":120,"sourceCode":"\t\tSkills []officialSkill `json:\"skills\"`\n\t}\n\n\tvar index officialIndex\n\tif err := json.Unmarshal([]byte(text), &index); err != nil {\n\t\treturn nil, err\n\t}\n\n\tseen := map[string]bool{}\n\tfor _, skill := range index.Skills {\n\t\tcandidate := strings.TrimSpace(skill.Name)\n\t\tif !skillNamePattern.MatchString(candidate) {\n\t\t\treturn nil, fmt.Errorf(\"invalid skill name %q\", candidate)\n\t\t}\n\t\tif skill.Type != \"archive\" || strings.TrimSpace(skill.URL) == \"\" || !digestPattern.MatchString(skill.Digest) {\n\t\t\treturn nil, fmt.Errorf(\"skill %s is not a complete v0.2 archive entry\", candidate)\n\t\t}\n\t\tif seen[candidate] {\n\t\t\treturn nil, fmt.Errorf(\"duplicate skill %s\", candidate)\n\t\t}\n\t\tseen[candidate] = true\n\t}\n\n\treturn sortedKeys(seen), nil\n}\n\n// parseGlobalSkillsList parses the output of \"npx -y skills ls -g\"\nfunc parseGlobalSkillsList(lines []string) []string {\n\tseen := map[string]bool{}\n\n\tfor _, line := range lines {\n\t\ttrimmed := strings.TrimSpace(line)\n\n\t\t// Skip header\n\t\tif strings.HasPrefix(trimmed, \"Global Skills\") {\n\t\t\tcontinue\n\t\t}","sourceCodeStart":102,"sourceCodeEnd":138,"githubUrl":"https://github.com/larksuite/cli/blob/7fd6ef3c07182257ce776cdc5a614e122d5bd4b3/internal/skillscheck/sync.go#L102-L138","documentation":"ParseOfficialSkillsIndexJSON rejects duplicate skill names using a seen set; after all other per-entry checks pass, an entry whose trimmed name was already accepted triggers this error. Duplicate names would make installation paths and references ambiguous, so the parser fails closed.","triggerScenarios":"fetchOfficialSkills parsing an index containing two entries with the same (trimmed) skill name, e.g. the same skill listed twice or two entries differing only in surrounding whitespace.","commonSituations":"An index generator bug appending an entry twice; a merge conflict in the index file resolved badly; case/whitespace variants that trim down to identical names; a mirror concatenating two index versions.","solutions":["Deduplicate the index so each skill name appears exactly once, keeping the entry with the newest digest","Check the index generation pipeline for accidental double-listing or merge artifacts","If names collide after trimming/normalization, rename one of the skills to a distinct kebab-case name","Fetch the index fresh from the official source in case a cached merged copy is stale"],"exampleFix":"// before\n[{\"name\":\"crm-sync\",...},{\"name\":\" crm-sync\",...}]\n// after\n[{\"name\":\"crm-sync\",...}]","handlingStrategy":"validation","validationCode":"seen := map[string]bool{}\nvar idx struct{ Skills []struct{ Name string `json:\"name\"` } `json:\"skills\"` } `json:\"skills\"`\nif err := json.Unmarshal(indexData, &idx); err == nil {\n    for _, s := range idx.Skills {\n        n := strings.TrimSpace(s.Name)\n        if seen[n] { /* duplicate: dedupe before parsing */ }\n        seen[n] = true\n    }\n}","typeGuard":null,"tryCatchPattern":"skills, err := skillscheck.ParseOfficialSkillsIndexJSON(data)\nif err != nil {\n    if strings.Contains(err.Error(), \"duplicate skill\") {\n        // refetch a clean index or dedupe locally before retry\n    }\n    return err\n}","preventionTips":["Dedupe entries by trimmed name in the index generation pipeline","Fix merge conflicts in index files by keeping one entry per skill","Avoid whitespace-only name variants that normalize to the same key","Add a uniqueness check to any custom index CI before publishing"],"tags":["validation","json","skills-index","duplicates"],"backgroundTag":"duplicate-entry-detected","analyzedSha":"7fd6ef3c07182257ce776cdc5a614e122d5bd4b3","analyzedAt":"2026-09-04T21:17:44.649Z","contentChangedAt":"2026-09-04T21:17:44.649Z","schemaVersion":2},"datasetVersion":"2026-09-12T02:17:10.037Z"}