11//go:build ignore
22
3- // score.go -- migration scoring script for the APM CLI Python-to-Go migration.
4- // Usage: go test -json ./... | go run .crane/scripts/score.go
5- // Outputs JSON with migration_score and progress metrics.
3+ // score.go -- deletion-grade migration scoring for the APM CLI Python-to-Go migration.
64//
7- // Scoring formula:
8- // migration_score = (parity_passing / parity_total) * correctness_gate
9- // correctness_gate = 1.0 if all target tests pass, 0.0 otherwise
5+ // Usage:
6+ // APM_PYTHON_BIN=/path/to/apm go test -count=1 -json ./... | go run .crane/scripts/score.go
107//
11- // NOTE: This script must NOT be modified after milestone 1 is accepted.
8+ // This script implements the deletion-grade framework from issues #78 and #96.
9+ // migration_score = 1.0 only when ALL of the following gates pass:
10+ //
11+ // Gate 1 -- python_reference_required: APM_PYTHON_BIN must be set and valid.
12+ // TestParityCompletionHardGate must PASS. A missing or invalid Python
13+ // binary is a hard failure -- never a warning or vacuous pass.
14+ //
15+ // Gate 2 -- go_tests_pass: every Go test in the module must pass. A single
16+ // failing non-parity test voids the gate.
17+ //
18+ // Gate 3 -- surface_parity: TestParityCompletionSurfaceParity must pass.
19+ // Python and Go command/option/subcommand inventories must match.
20+ //
21+ // Gate 4 -- help_parity: TestParityCompletionCommandMatrix and
22+ // TestParityCompletionHelpIdentical must pass. Every public help
23+ // and invalid-usage path must match Python.
24+ //
25+ // Gate 5 -- functional_contracts: TestParityCompletionFunctionalContracts
26+ // must pass. Supported command behavior must be covered by
27+ // black-box Python-vs-Go contracts.
28+ //
29+ // Gate 6 -- state_diff_contracts: TestParityCompletionStateDiffContracts
30+ // must pass. Mutating commands must match Python filesystem,
31+ // lockfile, config, cache, and generated-artifact effects.
32+ //
33+ // Gate 7 -- python_tests_pass: TestParityCompletionPythonSuite must pass.
34+ // The original Python reference test suite must still be green.
35+ //
36+ // Gate 8 -- benchmarks_pass: TestParityCompletionBenchmarks must pass.
37+ // Migration benchmarks must run and satisfy the configured guard.
38+ //
39+ // Gate 9 -- no_known_exceptions: the test output must not contain any
40+ // "approved exception" log line. Final cutover requires zero exceptions.
41+ //
42+ // If Gate 1 fails, migration_score is forced to 0.0 regardless of other gates.
43+ // Empty or all-skipped test streams also force migration_score to 0.0.
44+ //
45+ // The progress field shows the fraction of deletion-grade gates passing
46+ // (even when migration_score is 0 due to Gate 1 failure).
1247
1348package main
1449
@@ -27,20 +62,45 @@ type TestEvent struct {
2762 Output string `json:"Output"`
2863}
2964
65+ // GateResult tracks the pass/fail state of a single deletion-grade gate.
66+ type GateResult struct {
67+ Name string `json:"name"`
68+ Passing bool `json:"passing"`
69+ Reason string `json:"reason,omitempty"`
70+ }
71+
3072type Score struct {
31- MigrationScore float64 `json:"migration_score"`
32- Progress float64 `json:"progress"`
33- ParityPassing int `json:"parity_passing"`
34- ParityTotal int `json:"parity_total"`
35- SourceTestsPassing int `json:"source_tests_passing "`
36- TargetTestsPassing int `json:"target_tests_passing "`
37- PerfRatio float64 `json:"perf_ratio "`
73+ MigrationScore float64 `json:"migration_score"`
74+ Progress float64 `json:"progress"`
75+ ParityPassing int `json:"parity_passing"`
76+ ParityTotal int `json:"parity_total"`
77+ GoTestsTotal int `json:"go_tests_total "`
78+ GoTestsPassing int `json:"go_tests_passing "`
79+ Gates [] GateResult `json:"gates "`
3880}
3981
4082func main () {
4183 scanner := bufio .NewScanner (os .Stdin )
84+ scanner .Buffer (make ([]byte , 4 * 1024 * 1024 ), 4 * 1024 * 1024 )
85+
86+ // Deletion-grade gate test names.
87+ const (
88+ gateHardGate = "TestParityCompletionHardGate"
89+ gateSurfaceParity = "TestParityCompletionSurfaceParity"
90+ gateCommandMatrix = "TestParityCompletionCommandMatrix"
91+ gateHelpIdentical = "TestParityCompletionHelpIdentical"
92+ gateFunctionalContracts = "TestParityCompletionFunctionalContracts"
93+ gateStateDiffContracts = "TestParityCompletionStateDiffContracts"
94+ gatePythonSuite = "TestParityCompletionPythonSuite"
95+ gateBenchmarks = "TestParityCompletionBenchmarks"
96+ )
4297
43- var parityPassing , parityTotal , targetPassing , targetTotal int
98+ // Track per-test pass/fail.
99+ testPassed := map [string ]bool {}
100+ testFailed := map [string ]bool {}
101+ var totalTests , passingTests int
102+ knownExceptionsFound := false
103+ anyEvents := false
44104
45105 for scanner .Scan () {
46106 line := scanner .Text ()
@@ -51,56 +111,169 @@ func main() {
51111 if err := json .Unmarshal ([]byte (line ), & ev ); err != nil {
52112 continue
53113 }
114+
115+ anyEvents = true
116+
117+ // Scan output lines for approved-exception markers.
118+ // Tests log "APPROVED-EXCEPTION:" via t.Logf; final cutover requires zero.
119+ if ev .Action == "output" && ev .Output != "" {
120+ if strings .Contains (ev .Output , "APPROVED-EXCEPTION" ) {
121+ knownExceptionsFound = true
122+ }
123+ }
124+
54125 if ev .Test == "" {
55126 continue
56127 }
57128
58- isParity := strings .Contains (ev .Test , "Parity" ) || strings .Contains (ev .Package , "parity" )
59- isTarget := strings .HasPrefix (ev .Package , "github.com/githubnext/apm/" )
129+ switch ev .Action {
130+ case "run" :
131+ totalTests ++
132+ case "pass" :
133+ passingTests ++
134+ testPassed [ev .Test ] = true
135+ case "fail" :
136+ testFailed [ev .Test ] = true
137+ }
138+ }
139+
140+ // Gate 1: python_reference_required
141+ gate1 := GateResult {Name : "python_reference_required" }
142+ if ! anyEvents {
143+ gate1 .Passing = false
144+ gate1 .Reason = "empty test stream -- no test events received"
145+ } else if testFailed [gateHardGate ] {
146+ gate1 .Passing = false
147+ gate1 .Reason = "TestParityCompletionHardGate failed -- APM_PYTHON_BIN missing or invalid"
148+ } else if testPassed [gateHardGate ] {
149+ gate1 .Passing = true
150+ } else {
151+ gate1 .Passing = false
152+ gate1 .Reason = "TestParityCompletionHardGate not found in test stream"
153+ }
154+
155+ // Gate 2: go_tests_pass
156+ gate2 := GateResult {Name : "go_tests_pass" }
157+ if totalTests == 0 {
158+ gate2 .Passing = false
159+ gate2 .Reason = "no tests ran"
160+ } else if passingTests == totalTests {
161+ gate2 .Passing = true
162+ } else {
163+ gate2 .Passing = false
164+ gate2 .Reason = fmt .Sprintf ("%d/%d tests passing" , passingTests , totalTests )
165+ }
166+
167+ // Gate 3: surface_parity
168+ gate3 := singleTestGate ("surface_parity" , gateSurfaceParity , testPassed , testFailed )
169+
170+ // Gate 4: help_parity
171+ gate4 := multiTestGate (
172+ "help_parity" ,
173+ []string {gateCommandMatrix , gateHelpIdentical },
174+ testPassed ,
175+ testFailed ,
176+ )
60177
61- if isParity {
62- if ev .Action == "run" {
63- parityTotal ++
64- } else if ev .Action == "pass" {
178+ // Gate 5: functional_contracts
179+ gate5 := singleTestGate ("functional_contracts" , gateFunctionalContracts , testPassed , testFailed )
180+
181+ // Gate 6: state_diff_contracts
182+ gate6 := singleTestGate ("state_diff_contracts" , gateStateDiffContracts , testPassed , testFailed )
183+
184+ // Gate 7: python_tests_pass
185+ gate7 := singleTestGate ("python_tests_pass" , gatePythonSuite , testPassed , testFailed )
186+
187+ // Gate 8: benchmarks_pass
188+ gate8 := singleTestGate ("benchmarks_pass" , gateBenchmarks , testPassed , testFailed )
189+
190+ // Gate 9: no_known_exceptions
191+ gate9 := GateResult {Name : "no_known_exceptions" }
192+ if knownExceptionsFound {
193+ gate9 .Passing = false
194+ gate9 .Reason = "output contains 'approved exception' -- all exceptions must be resolved for cutover"
195+ } else {
196+ gate9 .Passing = true
197+ }
198+
199+ gates := []GateResult {gate1 , gate2 , gate3 , gate4 , gate5 , gate6 , gate7 , gate8 , gate9 }
200+
201+ // Count parity tests (any test with "Parity" in name from cmd/apm).
202+ parityPassing , parityTotal := 0 , 0
203+ for name , passed := range testPassed {
204+ if strings .Contains (name , "Parity" ) {
205+ parityTotal ++
206+ if passed {
65207 parityPassing ++
66208 }
67209 }
68- if isTarget {
69- if ev .Action == "run" {
70- targetTotal ++
71- } else if ev .Action == "pass" {
72- targetPassing ++
73- }
74- }
75210 }
76-
77- correctnessGate := 1.0
78- if targetTotal > 0 && targetPassing < targetTotal {
79- correctnessGate = 0.0
211+ for name := range testFailed {
212+ if strings . Contains ( name , "Parity" ) && ! testPassed [ name ] {
213+ parityTotal ++
214+ }
80215 }
81216
82- total := 302 // fixed: total Python modules/functions to port
83- if parityTotal > total {
84- total = parityTotal
217+ // Compute migration score.
218+ gatesPassing := 0
219+ for _ , g := range gates {
220+ if g .Passing {
221+ gatesPassing ++
222+ }
85223 }
224+ progress := float64 (gatesPassing ) / float64 (len (gates ))
86225
87226 var migrationScore float64
88- if total > 0 {
89- migrationScore = (float64 (parityPassing ) / float64 (total )) * correctnessGate
227+ if ! gate1 .Passing {
228+ // Hard gate: Python reference missing forces score to 0.
229+ migrationScore = 0.0
230+ } else {
231+ // All gates must pass for score 1.0; partial credit by gate fraction.
232+ migrationScore = progress
90233 }
91234
92- progress := float64 (parityPassing ) / float64 (total )
93-
94235 score := Score {
95- MigrationScore : migrationScore ,
96- Progress : progress ,
97- ParityPassing : parityPassing ,
98- ParityTotal : total ,
99- SourceTestsPassing : 247 , // stable Python baseline
100- TargetTestsPassing : targetPassing ,
101- PerfRatio : 1.0 ,
236+ MigrationScore : migrationScore ,
237+ Progress : progress ,
238+ ParityPassing : parityPassing ,
239+ ParityTotal : parityTotal ,
240+ GoTestsTotal : totalTests ,
241+ GoTestsPassing : passingTests ,
242+ Gates : gates ,
102243 }
103244
104245 out , _ := json .MarshalIndent (score , "" , " " )
105246 fmt .Println (string (out ))
106247}
248+
249+ func singleTestGate (name , testName string , testPassed , testFailed map [string ]bool ) GateResult {
250+ return multiTestGate (name , []string {testName }, testPassed , testFailed )
251+ }
252+
253+ func multiTestGate (name string , testNames []string , testPassed , testFailed map [string ]bool ) GateResult {
254+ for _ , testName := range testNames {
255+ if testFailed [testName ] {
256+ return GateResult {
257+ Name : name ,
258+ Passing : false ,
259+ Reason : testName + " failed" ,
260+ }
261+ }
262+ }
263+
264+ var missing []string
265+ for _ , testName := range testNames {
266+ if ! testPassed [testName ] {
267+ missing = append (missing , testName )
268+ }
269+ }
270+ if len (missing ) > 0 {
271+ return GateResult {
272+ Name : name ,
273+ Passing : false ,
274+ Reason : strings .Join (missing , ", " ) + " not found" ,
275+ }
276+ }
277+
278+ return GateResult {Name : name , Passing : true }
279+ }
0 commit comments