Skip to content

Commit 68c3799

Browse files
[Crane: crane-migration-python-to-go-full-apm-cli-rewrite] Iteration 9: Milestone 7 (core/ partial) -- errors, scope, target detection, apm_yml
Port core/errors.py (TargetResolutionError hierarchy + renderers), core/scope.py (InstallScope enum + path helpers), core/target_detection.py (DetectTarget, ShouldCompile*, NormalizeTargetList, GetTargetDescription), and core/apm_yml.py (ParseTargetsField with CSV/list sugar and validation). Added 26 TestParity* tests across 4 files; all 136 Go tests pass. Score: 0.4470 (was 0.3609, delta +0.0861). Run: https://github.com/githubnext/apm/actions/runs/26326750518 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 45d7b5e commit 68c3799

6 files changed

Lines changed: 936 additions & 0 deletions

File tree

internal/core/apm_yml.go

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
package core
2+
3+
import "fmt"
4+
5+
// CanonicalTargets is the set of target names accepted by APM.
6+
var CanonicalTargets = map[string]bool{
7+
"claude": true,
8+
"copilot": true,
9+
"cursor": true,
10+
"opencode": true,
11+
"codex": true,
12+
"gemini": true,
13+
"windsurf": true,
14+
"agent-skills": true,
15+
}
16+
17+
// ParseTargetsField parses the targets/target field from a raw apm.yml data
18+
// map. Returns a canonical list of target names. An empty list means neither
19+
// key was present (caller should fall through to auto-detect).
20+
func ParseTargetsField(yamlData map[string]interface{}) ([]string, error) {
21+
_, hasTargets := yamlData["targets"]
22+
_, hasTarget := yamlData["target"]
23+
24+
if hasTargets && hasTarget {
25+
return nil, NewConflictingTargetsError()
26+
}
27+
28+
if hasTargets {
29+
raw := yamlData["targets"]
30+
switch v := raw.(type) {
31+
case nil:
32+
return nil, NewEmptyTargetsListError()
33+
case []interface{}:
34+
if len(v) == 0 {
35+
return nil, NewEmptyTargetsListError()
36+
}
37+
tokens := make([]string, 0, len(v))
38+
for _, item := range v {
39+
t := fmt.Sprintf("%v", item)
40+
if t != "" {
41+
tokens = append(tokens, t)
42+
}
43+
}
44+
if err := validateCanonical(tokens); err != nil {
45+
return nil, err
46+
}
47+
return tokens, nil
48+
default:
49+
// Single value under targets key
50+
tokens := []string{fmt.Sprintf("%v", v)}
51+
if err := validateCanonical(tokens); err != nil {
52+
return nil, err
53+
}
54+
return tokens, nil
55+
}
56+
}
57+
58+
if hasTarget {
59+
raw := yamlData["target"]
60+
if raw == nil {
61+
return []string{}, nil
62+
}
63+
switch v := raw.(type) {
64+
case []interface{}:
65+
tokens := make([]string, 0, len(v))
66+
for _, item := range v {
67+
t := fmt.Sprintf("%v", item)
68+
if t != "" {
69+
tokens = append(tokens, t)
70+
}
71+
}
72+
if len(tokens) == 0 {
73+
return []string{}, nil
74+
}
75+
if err := validateCanonical(tokens); err != nil {
76+
return nil, err
77+
}
78+
return tokens, nil
79+
default:
80+
rawStr := fmt.Sprintf("%v", v)
81+
if rawStr == "" {
82+
return []string{}, nil
83+
}
84+
// CSV sugar: "claude,copilot" -> ["claude", "copilot"]
85+
tokens := splitCSV(rawStr)
86+
if len(tokens) == 0 {
87+
return []string{}, nil
88+
}
89+
if err := validateCanonical(tokens); err != nil {
90+
return nil, err
91+
}
92+
return tokens, nil
93+
}
94+
}
95+
96+
return []string{}, nil
97+
}
98+
99+
// validateCanonical checks every token is in CanonicalTargets.
100+
func validateCanonical(tokens []string) error {
101+
valid := sortedKeys(CanonicalTargets)
102+
for _, t := range tokens {
103+
if !CanonicalTargets[t] {
104+
return NewUnknownTargetError(t, valid)
105+
}
106+
}
107+
return nil
108+
}

0 commit comments

Comments
 (0)