Skip to content

Commit f6c870d

Browse files
[Autoloop: python-to-go-migration] Iteration 41: Migrate command_logger and models/validation
Migrate 2 Python modules to Go: - src/apm_cli/core/command_logger.py (751 lines) -> internal/core/commandlogger - src/apm_cli/models/validation.py (800 lines) -> internal/models/validation Total new lines: +1551 Python lines migrated. Metric: 34.17% (previous: 32.00%, delta: +2.17%) commandlogger: CommandLogger and InstallLogger with full lifecycle methods (start, progress, success, error, warning, verbose, policy violation logging, install summary). Delegates to console package. validation: PackageType/PackageContentType enums, ValidationResult, DetectionEvidence, GatherDetectionEvidence, DetectPackageType, and ValidateAPMPackage with all 7 detection-cascade cases. Includes test suite (6 tests pass). Run: https://github.com/githubnext/apm/actions/runs/25859136824 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 19f9919 commit f6c870d

4 files changed

Lines changed: 1065 additions & 5 deletions

File tree

benchmarks/migration-status.json

Lines changed: 19 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"original_python_lines": 71696,
3-
"migrated_python_lines": 22945,
3+
"migrated_python_lines": 24496,
44
"migrated_modules": [
55
{
66
"module": "src/apm_cli/constants.py",
@@ -680,11 +680,25 @@
680680
"python_lines": 479,
681681
"status": "migrated",
682682
"notes": "Instruction integration with cursor/claude/windsurf format transforms"
683+
},
684+
{
685+
"module": "src/apm_cli/core/command_logger.py",
686+
"go_package": "internal/core/commandlogger",
687+
"python_lines": 751,
688+
"status": "migrated",
689+
"notes": "CLI command logger infrastructure with Install/Command loggers"
690+
},
691+
{
692+
"module": "src/apm_cli/models/validation.py",
693+
"go_package": "internal/models/validation",
694+
"python_lines": 800,
695+
"status": "migrated",
696+
"notes": "PackageType/ValidationResult enums and DetectPackageType logic"
683697
}
684698
],
685-
"last_updated": "2026-05-14T09:05:00Z",
686-
"iteration": 38,
687-
"python_lines_migrated_pct": 27.32,
699+
"last_updated": "2026-05-14T12:17:24Z",
700+
"iteration": 39,
701+
"python_lines_migrated_pct": 34.17,
688702
"modules_migrated": [
689703
"policy/models.py",
690704
"models/plugin.py",
@@ -734,4 +748,4 @@
734748
"status": "migrated"
735749
}
736750
]
737-
}
751+
}
Lines changed: 355 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,355 @@
1+
// Package commandlogger provides structured CLI output infrastructure for APM commands.
2+
//
3+
// Mirrors src/apm_cli/core/command_logger.py.
4+
package commandlogger
5+
6+
import (
7+
"fmt"
8+
9+
"github.com/githubnext/apm/internal/utils/console"
10+
)
11+
12+
// StripSourcePrefix removes the "org:" or "url:" prefix from a policy source string.
13+
func StripSourcePrefix(source string) string {
14+
if source == "" {
15+
return ""
16+
}
17+
for _, pfx := range []string{"org:", "url:"} {
18+
if len(source) > len(pfx) && source[:len(pfx)] == pfx {
19+
return source[len(pfx):]
20+
}
21+
}
22+
return source
23+
}
24+
25+
// CommandLogger is the base context-aware logger for all CLI commands.
26+
// All methods delegate to console helpers -- no new output primitives.
27+
type CommandLogger struct {
28+
Command string
29+
Verbose bool
30+
DryRun bool
31+
}
32+
33+
// NewCommandLogger creates a new CommandLogger.
34+
func NewCommandLogger(command string, verbose, dryRun bool) *CommandLogger {
35+
return &CommandLogger{Command: command, Verbose: verbose, DryRun: dryRun}
36+
}
37+
38+
// Start logs the start of an operation.
39+
func (l *CommandLogger) Start(message string) {
40+
console.Info(message, "running")
41+
}
42+
43+
// Progress logs progress during an operation.
44+
func (l *CommandLogger) Progress(message string) {
45+
console.Info(message, "info")
46+
}
47+
48+
// MCPLookupHeartbeat emits a single batch heartbeat before MCP registry validation.
49+
func (l *CommandLogger) MCPLookupHeartbeat(count int) {
50+
if count <= 0 {
51+
return
52+
}
53+
noun := "servers"
54+
if count == 1 {
55+
noun = "server"
56+
}
57+
console.Info(fmt.Sprintf("Looking up %d MCP %s in registry...", count, noun), "running")
58+
}
59+
60+
// Info logs static advisory/informational context.
61+
func (l *CommandLogger) Info(message, symbol string) {
62+
if symbol == "" {
63+
symbol = "info"
64+
}
65+
console.Info(message, symbol)
66+
}
67+
68+
// Success logs successful completion.
69+
func (l *CommandLogger) Success(message string) {
70+
console.Success(message, "sparkles")
71+
}
72+
73+
// Warning logs a warning.
74+
func (l *CommandLogger) Warning(message string) {
75+
console.Warning(message, "warning")
76+
}
77+
78+
// Error logs an error.
79+
func (l *CommandLogger) Error(message string) {
80+
console.Error(message, "error")
81+
}
82+
83+
// VerboseDetail logs a detail only when verbose mode is enabled.
84+
func (l *CommandLogger) VerboseDetail(message string) {
85+
if l.Verbose {
86+
console.Echo(nil, message, "dim", "", false)
87+
}
88+
}
89+
90+
// TreeItem logs a tree sub-item (continuation line) under a package block.
91+
func (l *CommandLogger) TreeItem(message string) {
92+
console.Echo(nil, message, "green", "", false)
93+
}
94+
95+
// BlankLine logs a blank line.
96+
func (l *CommandLogger) BlankLine() {
97+
console.Echo(nil, "", "", "", false)
98+
}
99+
100+
// PackageInlineWarning logs an inline warning under a package block (verbose only).
101+
func (l *CommandLogger) PackageInlineWarning(message string) {
102+
if l.Verbose {
103+
console.Echo(nil, message, "yellow", "", false)
104+
}
105+
}
106+
107+
// DryRunNotice logs what would happen in dry-run mode.
108+
func (l *CommandLogger) DryRunNotice(whatWouldHappen string) {
109+
console.Info(fmt.Sprintf("[dry-run] %s", whatWouldHappen), "info")
110+
}
111+
112+
// ShouldExecute returns false if in dry-run mode.
113+
func (l *CommandLogger) ShouldExecute() bool {
114+
return !l.DryRun
115+
}
116+
117+
// AuthStep logs an auth resolution step (verbose only).
118+
func (l *CommandLogger) AuthStep(step string, success bool, detail string) {
119+
if !l.Verbose {
120+
return
121+
}
122+
msg := fmt.Sprintf(" auth: %s", step)
123+
if detail != "" {
124+
msg += fmt.Sprintf(" (%s)", detail)
125+
}
126+
symbol := "check"
127+
if !success {
128+
symbol = "error"
129+
}
130+
console.Echo(nil, msg, "dim", symbol, false)
131+
}
132+
133+
// PolicyDiscoveryMiss logs a policy-discovery non-success outcome.
134+
func (l *CommandLogger) PolicyDiscoveryMiss(outcome, source, errText, hostOrg string) {
135+
if errText == "" {
136+
errText = "unknown"
137+
}
138+
switch outcome {
139+
case "absent":
140+
if !l.Verbose {
141+
return
142+
}
143+
org := hostOrg
144+
if org == "" {
145+
org = StripSourcePrefix(source)
146+
}
147+
if org == "" {
148+
org = "this project"
149+
}
150+
console.Info(fmt.Sprintf("No org policy found for %s", org), "info")
151+
152+
case "no_git_remote":
153+
if !l.Verbose {
154+
return
155+
}
156+
console.Info("Could not determine org from git remote; policy auto-discovery skipped", "info")
157+
158+
case "empty":
159+
src := source
160+
if src == "" {
161+
src = "this project"
162+
}
163+
console.Warning(fmt.Sprintf("Org policy at %s is present but empty; no enforcement applied", src), "warning")
164+
165+
case "malformed":
166+
console.Warning(fmt.Sprintf("Policy at %s is malformed: %s. Contact your org admin to fix the policy file.", source, errText), "warning")
167+
168+
case "cache_miss_fetch_fail":
169+
console.Warning(fmt.Sprintf("Could not fetch org policy from %s (%s); proceeding without policy enforcement. Retry, check connectivity, or use --no-policy to bypass.", source, errText), "warning")
170+
171+
case "garbage_response":
172+
console.Warning(fmt.Sprintf("Policy response from %s is not valid YAML (%s); proceeding without policy enforcement. Contact your org admin or use --no-policy.", source, errText), "warning")
173+
174+
case "cached_stale":
175+
console.Warning(fmt.Sprintf("Using stale cached policy (refresh failed: %s); enforcement still applies from cached policy.", errText), "warning")
176+
177+
case "hash_mismatch":
178+
console.Error(fmt.Sprintf("Policy hash mismatch: pinned hash does not match fetched policy (%s). Update apm.yml policy.hash or contact your org admin.", errText), "error")
179+
180+
default:
181+
if errText != "unknown" && errText != "" {
182+
console.Warning(fmt.Sprintf("Policy discovery issue: %s", errText), "warning")
183+
}
184+
}
185+
}
186+
187+
// PolicyViolation records a policy violation for a dependency.
188+
func (l *CommandLogger) PolicyViolation(depRef, reason, severity, source string) {
189+
// Strip depRef prefix if present.
190+
prefix := depRef + ": "
191+
if len(reason) > len(prefix) && reason[:len(prefix)] == prefix {
192+
reason = reason[len(prefix):]
193+
}
194+
if severity == "block" {
195+
console.Error(fmt.Sprintf("Policy violation: %s -- %s", depRef, reason), "error")
196+
if source != "" {
197+
msg := fmt.Sprintf(" Blocked by org policy at %s -- remove `%s` from apm.yml, contact admin to update policy, or use `--no-policy` for one-off bypass", source, depRef)
198+
console.Echo(nil, msg, "dim", "", false)
199+
}
200+
}
201+
}
202+
203+
// PolicyDisabled logs a loud warning that policy enforcement is disabled.
204+
func (l *CommandLogger) PolicyDisabled(reason string) {
205+
console.Warning(fmt.Sprintf("Policy enforcement disabled by %s for this invocation. This does NOT bypass apm audit --ci. CI will still fail the PR for the same policy violation.", reason), "warning")
206+
}
207+
208+
// InstallSummary logs the final install summary.
209+
func (l *CommandLogger) InstallSummary(apmCount, mcpCount, errors, staleCleaned int, elapsedSeconds float64, hasElapsed bool) {
210+
var parts []string
211+
if apmCount > 0 {
212+
noun := "dependencies"
213+
if apmCount == 1 {
214+
noun = "dependency"
215+
}
216+
parts = append(parts, fmt.Sprintf("%d APM %s", apmCount, noun))
217+
}
218+
if mcpCount > 0 {
219+
noun := "servers"
220+
if mcpCount == 1 {
221+
noun = "server"
222+
}
223+
parts = append(parts, fmt.Sprintf("%d MCP %s", mcpCount, noun))
224+
}
225+
226+
cleanupSuffix := ""
227+
if staleCleaned > 0 {
228+
fNoun := "files"
229+
if staleCleaned == 1 {
230+
fNoun = "file"
231+
}
232+
cleanupSuffix = fmt.Sprintf(" (%d stale %s cleaned)", staleCleaned, fNoun)
233+
}
234+
235+
timingSuffix := ""
236+
if hasElapsed {
237+
timingSuffix = fmt.Sprintf(" in %.1fs", elapsedSeconds)
238+
}
239+
240+
if len(parts) > 0 {
241+
summary := joinParts(parts)
242+
if errors > 0 {
243+
console.Warning(fmt.Sprintf("Installed %s%s%s with %d error(s).", summary, cleanupSuffix, timingSuffix, errors), "warning")
244+
} else {
245+
console.Success(fmt.Sprintf("Installed %s%s%s.", summary, cleanupSuffix, timingSuffix), "sparkles")
246+
}
247+
} else if errors > 0 {
248+
console.Error(fmt.Sprintf("Installation failed with %d error(s)%s.", errors, timingSuffix), "error")
249+
}
250+
}
251+
252+
func joinParts(parts []string) string {
253+
if len(parts) == 0 {
254+
return ""
255+
}
256+
if len(parts) == 1 {
257+
return parts[0]
258+
}
259+
return parts[0] + " and " + parts[1]
260+
}
261+
262+
// InstallInterrupted logs a minimal elapsed-time line for interrupted installs.
263+
func (l *CommandLogger) InstallInterrupted(elapsedSeconds float64) {
264+
console.Warning(fmt.Sprintf("Install interrupted after %.1fs.", elapsedSeconds), "warning")
265+
}
266+
267+
// InstallLogger is the install-specific logger with validation, resolution, and download phases.
268+
type InstallLogger struct {
269+
*CommandLogger
270+
Partial bool
271+
staleCleaned int
272+
}
273+
274+
// NewInstallLogger creates a new InstallLogger.
275+
func NewInstallLogger(verbose, dryRun, partial bool) *InstallLogger {
276+
return &InstallLogger{
277+
CommandLogger: NewCommandLogger("install", verbose, dryRun),
278+
Partial: partial,
279+
}
280+
}
281+
282+
// ValidationStart logs start of package validation.
283+
func (l *InstallLogger) ValidationStart(count int) {
284+
noun := "packages"
285+
if count == 1 {
286+
noun = "package"
287+
}
288+
console.Info(fmt.Sprintf("Validating %d %s...", count, noun), "gear")
289+
}
290+
291+
// ValidationPass logs a package that passed validation.
292+
func (l *InstallLogger) ValidationPass(canonical string, alreadyPresent bool) {
293+
if alreadyPresent {
294+
console.Echo(nil, fmt.Sprintf("%s (already in apm.yml)", canonical), "dim", "check", false)
295+
} else {
296+
console.Success(canonical, "check")
297+
}
298+
}
299+
300+
// ValidationFail logs a package that failed validation.
301+
func (l *InstallLogger) ValidationFail(pkg, reason string) {
302+
console.Error(fmt.Sprintf("%s -- %s", pkg, reason), "error")
303+
}
304+
305+
// ResolutionStart logs start of dependency resolution.
306+
func (l *InstallLogger) ResolutionStart(toInstallCount, lockfileCount int) {
307+
if l.Partial {
308+
noun := "packages"
309+
if toInstallCount == 1 {
310+
noun = "package"
311+
}
312+
console.Info(fmt.Sprintf("Installing %d new %s...", toInstallCount, noun), "running")
313+
if lockfileCount > 0 && l.Verbose {
314+
console.Echo(nil, fmt.Sprintf(" (%d existing dependencies in lockfile)", lockfileCount), "dim", "", false)
315+
}
316+
} else {
317+
console.Info("Installing dependencies from apm.yml...", "running")
318+
if lockfileCount > 0 {
319+
console.Info(fmt.Sprintf("Using apm.lock.yaml (%d locked dependencies)", lockfileCount), "")
320+
}
321+
}
322+
}
323+
324+
// NothingToInstall logs when there's nothing to install.
325+
func (l *InstallLogger) NothingToInstall(lockfilePresent, updateMode bool) {
326+
if l.Partial {
327+
console.Info("Requested packages are already installed.", "check")
328+
} else {
329+
console.Success("All dependencies are up to date.", "check")
330+
}
331+
if lockfilePresent && !updateMode {
332+
console.Info("Lockfile already satisfied -- run 'apm update' to resolve latest refs.", "")
333+
}
334+
}
335+
336+
// DownloadStart logs start of a package download.
337+
func (l *InstallLogger) DownloadStart(depName string, cached bool) {
338+
if cached {
339+
l.VerboseDetail(fmt.Sprintf(" Using cached: %s", depName))
340+
} else if l.Verbose {
341+
console.Info(fmt.Sprintf(" Downloading: %s", depName), "download")
342+
}
343+
}
344+
345+
// ResolvingHeartbeat emits a per-dependency progress heartbeat during BFS resolve.
346+
func (l *InstallLogger) ResolvingHeartbeat(depName string) {
347+
if l.Verbose {
348+
console.Info(fmt.Sprintf(" Resolving: %s", depName), "running")
349+
}
350+
}
351+
352+
// DownloadComplete logs completion of a package download.
353+
func (l *InstallLogger) DownloadComplete(depName string) {
354+
l.VerboseDetail(fmt.Sprintf(" Downloaded: %s", depName))
355+
}

0 commit comments

Comments
 (0)