Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
# Migration Plan: APM CLI Python to Go

## Strategy: Greenfield

The Go implementation is built in parallel alongside the existing Python codebase.
The Python version stays runnable for benchmark parity testing throughout.
End state is a clean Go binary with no Python in the shipping artifact.

## Milestones

| # | Milestone | Scope | Acceptance | Status |
|---|-----------|-------|------------|--------|
| 0 | Planning | Inventory, plan, scoring scaffold | Plan committed, score.go exists | done |
| 1 | Build scaffolding | go.mod, go.sum, cmd/apm/main.go stub, CI | `go build ./...` passes, CI green | todo |
| 2 | Go test/parity harness | acceptance tests calling Python binary, parity framework | score.go returns valid JSON, parity_total >= 10 | todo |
| 3 | utils/ + constants + config | internal/utils, internal/constants, internal/config | parity tests pass for all util functions | todo |
| 4 | models/ + primitives/ | internal/models, internal/primitives | parity tests pass for data structures | todo |
| 5 | deps/ | internal/deps -- dependency resolution | parity tests pass for dep resolution | todo |
| 6 | cache/ | internal/cache -- HTTP/git caching | parity tests pass for cache layer | todo |
| 7 | core/ | internal/core -- auth, target detection, orchestration | parity tests pass for core | todo |
| 8 | install/ | internal/install -- install pipeline and phases | parity tests pass for install | todo |
| 9 | commands/ | internal/commands -- cobra replacing click | all commands respond correctly | todo |
| 10 | integration/ | internal/integration -- file integrators | parity tests pass for integrators | todo |
| 11 | compilation/ | internal/compilation -- compilation pipeline | parity tests pass for compilation | todo |
| 12 | runtime/ | internal/runtime -- runtime adapters | parity tests pass | todo |
| 13 | policy/ + security/ | internal/policy, internal/security | parity tests pass | todo |
| 14 | marketplace/ + registry/ | internal/marketplace, internal/registry | parity tests pass | todo |
| 15 | bundle/ + output/ | internal/bundle, internal/output | parity tests pass | todo |
| 16 | CLI entry point wiring | cmd/apm/ final wiring | full CLI parity, migration_score = 1.0 | todo |

## Source Inventory Summary

- **302 Python files** across 20 modules
- Largest modules: install (49), commands (44), marketplace (28), deps (25), utils (20)
- Key external Python deps: click, rich, requests, pyyaml, gitpython, ruamel.yaml, watchdog

## Notes

- Never modify src/apm_cli/ (Python source) -- it is the parity reference
- Never modify tests/ -- Python test suite is the parity oracle
- The score.go script must not be modified after milestone 1 is accepted
- Target: migration_score = 1.0 (all parity tests passing, all Go tests passing)
106 changes: 106 additions & 0 deletions .crane/scripts/score.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
//go:build ignore

// score.go -- migration scoring script for the APM CLI Python-to-Go migration.
// Usage: go test -json ./... | go run .crane/scripts/score.go
// Outputs JSON with migration_score and progress metrics.
//
// Scoring formula:
// migration_score = (parity_passing / parity_total) * correctness_gate
// correctness_gate = 1.0 if all target tests pass, 0.0 otherwise
//
// NOTE: This script must NOT be modified after milestone 1 is accepted.

package main

import (
"bufio"
"encoding/json"
"fmt"
"os"
"strings"
)

type TestEvent struct {
Action string `json:"Action"`
Package string `json:"Package"`
Test string `json:"Test"`
Output string `json:"Output"`
}

type Score struct {
MigrationScore float64 `json:"migration_score"`
Progress float64 `json:"progress"`
ParityPassing int `json:"parity_passing"`
ParityTotal int `json:"parity_total"`
SourceTestsPassing int `json:"source_tests_passing"`
TargetTestsPassing int `json:"target_tests_passing"`
PerfRatio float64 `json:"perf_ratio"`
}

func main() {
scanner := bufio.NewScanner(os.Stdin)

var parityPassing, parityTotal, targetPassing, targetTotal int

for scanner.Scan() {
line := scanner.Text()
if !strings.HasPrefix(line, "{") {
continue
}
var ev TestEvent
if err := json.Unmarshal([]byte(line), &ev); err != nil {
continue
}
if ev.Test == "" {
continue
}

isParity := strings.Contains(ev.Test, "Parity") || strings.Contains(ev.Package, "parity")
isTarget := strings.HasPrefix(ev.Package, "github.com/githubnext/apm/")

if isParity {
if ev.Action == "run" {
parityTotal++
} else if ev.Action == "pass" {
parityPassing++
}
}
if isTarget {
if ev.Action == "run" {
targetTotal++
} else if ev.Action == "pass" {
targetPassing++
}
}
}

correctnessGate := 1.0
if targetTotal > 0 && targetPassing < targetTotal {
correctnessGate = 0.0
}

total := 302 // fixed: total Python modules/functions to port
if parityTotal > total {
total = parityTotal
}

var migrationScore float64
if total > 0 {
migrationScore = (float64(parityPassing) / float64(total)) * correctnessGate
}

progress := float64(parityPassing) / float64(total)

score := Score{
MigrationScore: migrationScore,
Progress: progress,
ParityPassing: parityPassing,
ParityTotal: total,
SourceTestsPassing: 247, // stable Python baseline
TargetTestsPassing: targetPassing,
PerfRatio: 1.0,
}

out, _ := json.MarshalIndent(score, "", " ")
fmt.Println(string(out))
}
59 changes: 59 additions & 0 deletions tests/unit/test_crane_score.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
from __future__ import annotations

import json
import shutil
import subprocess
from pathlib import Path

import pytest

ROOT = Path(__file__).resolve().parents[2]


def _run_score(input_lines: list[str]) -> dict[str, object]:
if shutil.which("go") is None:
pytest.skip("Go toolchain is not installed")

result = subprocess.run(
["go", "run", ".crane/scripts/score.go"],
cwd=ROOT,
input="\n".join(input_lines) + "\n",
text=True,
capture_output=True,
check=True,
)
return json.loads(result.stdout)


def test_crane_score_counts_parity_events() -> None:
score = _run_score(
[
"not json",
'{"Action":"run","Package":"github.com/githubnext/apm/internal/parity","Test":"TestInstallParity"}',
'{"Action":"pass","Package":"github.com/githubnext/apm/internal/parity","Test":"TestInstallParity"}',
'{"Action":"run","Package":"github.com/githubnext/apm/internal/parity","Test":"TestCompileParity"}',
'{"Action":"pass","Package":"github.com/githubnext/apm/internal/parity","Test":"TestCompileParity"}',
]
)

assert score["migration_score"] == pytest.approx(2 / 302)
assert score["progress"] == pytest.approx(2 / 302)
assert score["parity_passing"] == 2
assert score["parity_total"] == 302
assert score["source_tests_passing"] == 247
assert score["target_tests_passing"] == 2
assert score["perf_ratio"] == 1.0


def test_crane_score_applies_target_correctness_gate() -> None:
score = _run_score(
[
'{"Action":"run","Package":"github.com/githubnext/apm/internal/parity","Test":"TestInstallParity"}',
'{"Action":"pass","Package":"github.com/githubnext/apm/internal/parity","Test":"TestInstallParity"}',
'{"Action":"run","Package":"github.com/githubnext/apm/internal/config","Test":"TestConfig"}',
]
)

assert score["migration_score"] == 0
assert score["progress"] == pytest.approx(1 / 302)
assert score["target_tests_passing"] == 1
Loading