Skip to content

Commit f4b4f12

Browse files
committed
Merge remote-tracking branch 'origin/main' into codex/golden-fixture-completion-gate
# Conflicts: # cmd/apm/CUTOVER.md # tests/parity/python_contract_coverage.yml # tests/unit/test_crane_score.py
2 parents 190b4f2 + e0eaf25 commit f4b4f12

17 files changed

Lines changed: 1342 additions & 294 deletions

.crane/scripts/score.go

Lines changed: 11 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -143,7 +143,7 @@ func computeScore(input scanInput, getenv getenvFunc) (Score, error) {
143143
knownExceptions := knownExceptionsFromEnv(getenv("APM_KNOWN_EXCEPTIONS"))
144144
pythonReference := BoolGate{}
145145
pythonTests := BoolGate{Seen: getenv("APM_PYTHON_TESTS") != "", Passed: getenv("APM_PYTHON_TESTS") == "pass"}
146-
benchmarks := BoolGate{Seen: getenv("APM_BENCHMARKS") != "", Passed: getenv("APM_BENCHMARKS") == "pass"}
146+
benchmarks := RatioGate{}
147147
surface := RatioGate{}
148148
help := RatioGate{}
149149
functional := RatioGate{}
@@ -261,25 +261,25 @@ func computeScore(input scanInput, getenv getenvFunc) (Score, error) {
261261
pythonReference = BoolGate{Seen: true, Passed: testPassed(passed, failed, "TestParityCompletionHardGate") || pythonReferenceReady(getenv("APM_PYTHON_BIN"))}
262262
}
263263
if !surface.Seen {
264-
surface = inferredAnyRatioGate(passed, failed, "TestParityCompletionSurfaceParity", "TestParitySurfaceInventory")
264+
surface = missingRatioGate()
265265
}
266266
if !help.Seen {
267-
help = inferredAllRatioGate(passed, failed, "TestParityCompletionCommandMatrix", "TestParityCompletionHelpIdentical")
267+
help = missingRatioGate()
268268
}
269269
if !functional.Seen {
270-
functional = inferredAnyRatioGate(passed, failed, "TestParityCompletionFunctionalContracts", "TestParityFunctionalContracts")
270+
functional = missingRatioGate()
271271
}
272272
if !stateDiff.Seen {
273-
stateDiff = inferredAnyRatioGate(passed, failed, "TestParityCompletionStateDiffContracts", "TestParityStateDiffContracts")
273+
stateDiff = missingRatioGate()
274274
}
275275
if !behaviorContracts.Seen {
276-
behaviorContracts = RatioGate{Seen: true, Passing: 0, Total: 1}
276+
behaviorContracts = missingRatioGate()
277277
}
278278
if !pythonTests.Seen {
279279
pythonTests = BoolGate{Seen: true, Passed: testPassed(passed, failed, "TestParityCompletionPythonSuite")}
280280
}
281281
if !benchmarks.Seen {
282-
benchmarks = BoolGate{Seen: true, Passed: testPassed(passed, failed, "TestParityCompletionBenchmarks")}
282+
benchmarks = missingRatioGate()
283283
}
284284

285285
goTestsPass := !goTestsFailed && targetTotal > 0 && targetPassing == targetTotal
@@ -395,7 +395,7 @@ func applyGateEvent(
395395
noPythonRuntime *BoolGate,
396396
knownExceptions *int,
397397
pythonTests *BoolGate,
398-
benchmarks *BoolGate,
398+
benchmarks *RatioGate,
399399
) {
400400
switch gate.Name {
401401
case "python_reference":
@@ -421,7 +421,7 @@ func applyGateEvent(
421421
case "python_tests":
422422
*pythonTests = BoolGate{Seen: true, Passed: gate.Passed}
423423
case "benchmarks":
424-
*benchmarks = BoolGate{Seen: true, Passed: gate.Passed}
424+
*benchmarks = RatioGate{Seen: true, Passing: gate.Passing, Total: gate.Total}
425425
}
426426
}
427427

@@ -454,31 +454,8 @@ func testPassed(passed, failed map[string]bool, names ...string) bool {
454454
return false
455455
}
456456

457-
func inferredAnyRatioGate(passed, failed map[string]bool, names ...string) RatioGate {
458-
for _, name := range names {
459-
if failed[name] {
460-
return RatioGate{Seen: true, Passing: 0, Total: 1}
461-
}
462-
}
463-
return RatioGate{Seen: true, Passing: boolToInt(testPassed(passed, failed, names...)), Total: 1}
464-
}
465-
466-
func inferredAllRatioGate(passed, failed map[string]bool, names ...string) RatioGate {
467-
for _, name := range names {
468-
if failed[name] {
469-
return RatioGate{Seen: true, Passing: 0, Total: 1}
470-
}
471-
}
472-
return RatioGate{Seen: true, Passing: boolToInt(allRequiredTestsPassed(passed, names...)), Total: 1}
473-
}
474-
475-
func allRequiredTestsPassed(passed map[string]bool, names ...string) bool {
476-
for _, name := range names {
477-
if !passed[name] {
478-
return false
479-
}
480-
}
481-
return true
457+
func missingRatioGate() RatioGate {
458+
return RatioGate{Seen: true, Passing: 0, Total: 1}
482459
}
483460

484461
func gateResults(gates CutoverGates) []GateResult {
@@ -506,13 +483,6 @@ func passFail(ok bool) string {
506483
return "fail"
507484
}
508485

509-
func boolToInt(ok bool) int {
510-
if ok {
511-
return 1
512-
}
513-
return 0
514-
}
515-
516486
func knownExceptionsFromEnv(raw string) int {
517487
if raw == "" {
518488
return 0

.github/workflows/migration-ci.yml

Lines changed: 53 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,12 @@ on:
44
pull_request:
55
branches: [main]
66
workflow_dispatch:
7+
inputs:
8+
enforce_completion:
9+
description: "Fail unless migration completion gates are fully satisfied"
10+
required: false
11+
default: false
12+
type: boolean
713

814
permissions:
915
contents: read
@@ -99,6 +105,18 @@ jobs:
99105
- name: Run Go parity tests
100106
shell: bash
101107
run: |
108+
enforce_completion=false
109+
if [ "${{ github.event_name }}" = "workflow_dispatch" ] && [ "${{ inputs.enforce_completion == true }}" = "true" ]; then
110+
enforce_completion=true
111+
elif [ "${{ github.event_name }}" = "pull_request" ] && [[ "${{ github.event.pull_request.head.ref }}" == crane/* ]]; then
112+
enforce_completion=true
113+
fi
114+
115+
echo "MIGRATION_COMPLETION_ENFORCED=$enforce_completion" >> "$GITHUB_ENV"
116+
if [ "$enforce_completion" = "true" ]; then
117+
export APM_ENFORCE_COMPLETION_GATES=1
118+
fi
119+
102120
set +e
103121
go test -json ./... | tee "$RUNNER_TEMP/go-test-events.json"
104122
status=${PIPESTATUS[0]}
@@ -113,21 +131,38 @@ jobs:
113131
--coverage tests/parity/python_contract_coverage.yml \
114132
--allow-intentionally-incomplete \
115133
--summary "$RUNNER_TEMP/python-contract-coverage.md" || true
116-
python - "$RUNNER_TEMP/migration-score.json" <<'PY'
134+
python - "$RUNNER_TEMP/migration-score.json" "${MIGRATION_COMPLETION_ENFORCED:-false}" <<'PY'
117135
import json
118136
import sys
119137
120138
with open(sys.argv[1], encoding="utf-8") as fh:
121139
score = json.load(fh)
140+
enforce_completion = sys.argv[2].lower() == "true"
122141
123142
print(json.dumps(score, indent=2, sort_keys=True))
143+
if not enforce_completion:
144+
print(
145+
"::notice::Non-enforcing migration evidence run; "
146+
"completion gates are enforced only for crane/* PRs and "
147+
"manual runs with enforce_completion=true."
148+
)
149+
raise SystemExit(0)
124150
if score.get("progress") != 1.0:
125151
raise SystemExit("progress must be 1.0 for completion parity")
126152
if score.get("migration_score") == 1.0 and not score.get("deletion_grade_ready"):
127153
raise SystemExit("migration_score 1.0 requires deletion_grade_ready")
128154
PY
129-
test "${PYTHON_CLI_CONTRACT_STATUS:-1}" = "0"
130-
test "${GO_TEST_STATUS:-1}" = "0"
155+
if [ "${MIGRATION_COMPLETION_ENFORCED:-false}" = "true" ]; then
156+
test "${PYTHON_CLI_CONTRACT_STATUS:-1}" = "0"
157+
test "${GO_TEST_STATUS:-1}" = "0"
158+
else
159+
if [ "${PYTHON_CLI_CONTRACT_STATUS:-1}" != "0" ]; then
160+
echo "::notice::Python behavior contract tests are incomplete in collection mode."
161+
fi
162+
if [ "${GO_TEST_STATUS:-1}" != "0" ]; then
163+
echo "::notice::Go parity tests are incomplete in collection mode."
164+
fi
165+
fi
131166
132167
- name: Upload parity evidence
133168
if: always()
@@ -171,13 +206,27 @@ jobs:
171206
run: go build -o "$RUNNER_TEMP/apm-go" ./cmd/apm
172207

173208
- name: Run Python-vs-Go CLI benchmark
209+
shell: bash
174210
run: |
211+
enforce_completion=false
212+
if [ "${{ github.event_name }}" = "workflow_dispatch" ] && [ "${{ inputs.enforce_completion == true }}" = "true" ]; then
213+
enforce_completion=true
214+
elif [ "${{ github.event_name }}" = "pull_request" ] && [[ "${{ github.event.pull_request.head.ref }}" == crane/* ]]; then
215+
enforce_completion=true
216+
fi
217+
218+
extra_args=()
219+
if [ "$enforce_completion" != "true" ]; then
220+
extra_args+=(--allow-failures)
221+
fi
222+
175223
python scripts/ci/migration_cli_benchmark.py \
176224
--python-bin "$GITHUB_WORKSPACE/.venv/bin/apm" \
177225
--go-bin "$RUNNER_TEMP/apm-go" \
178226
--json-out "$RUNNER_TEMP/migration-cli-benchmark.json" \
179227
--markdown-out "$RUNNER_TEMP/migration-cli-benchmark.md" \
180-
--max-ratio 5.0
228+
--max-ratio 5.0 \
229+
"${extra_args[@]}"
181230
182231
- name: Run Python scaling guards
183232
run: uv run pytest tests/benchmarks/test_scaling_guards.py -v

README.md

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -61,10 +61,20 @@ Maintainers can dispatch the migration workflow manually:
6161
gh workflow run migration-ci.yml --repo githubnext/apm --ref main
6262
```
6363

64+
That default manual run collects parity and benchmark evidence without treating
65+
known migration gaps as a CI failure. To run the deterministic hard completion
66+
gate, opt in explicitly:
67+
68+
```bash
69+
gh workflow run migration-ci.yml --repo githubnext/apm --ref main -f enforce_completion=true
70+
```
71+
6472
After it runs, open the **Migration Benchmarks** job summary for the timing
6573
table. The same run uploads the `migration-benchmark-evidence` artifact with
6674
JSON and Markdown copies of the benchmark data. In the benchmark table, the
6775
`Go/Python` ratio is the Go median duration divided by the Python median
68-
duration: values below `1.00x` mean Go is faster. Recent smoke benchmark
69-
evidence for startup/help/init-style commands shows the Go CLI roughly
70-
`327x`-`370x` faster than the Python CLI.
76+
duration: values below `1.00x` mean Go is faster. The benchmark includes
77+
fixture-backed commands that read, write, execute, or fail against realistic APM
78+
project state: `apm.yml`, `apm.lock.yaml`, installed `apm_modules`, local
79+
`.apm` primitives, target directories, deployed prompt files, and sample source
80+
files.

cmd/apm/CUTOVER.md

Lines changed: 63 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,59 @@ cutover corpus, not final completion proof. Final completion requires the
2020
full command matrix below to be represented as committed fixtures and replayed
2121
by Go without invoking the Python runtime.
2222

23-
Remaining commands return a "not yet fully implemented" message.
23+
Most remaining commands are wired at the CLI surface. That is not enough for
24+
cutover. A command that prints success without writing the expected files,
25+
mutating `apm.yml`, updating `apm.lock.yaml`, executing a script, or detecting a
26+
planted failure is still incomplete.
27+
28+
## Real Criteria
29+
30+
Every completion criterion must be backed by real command execution. The scorer
31+
does not infer completion from test names for `surface`, `help`, `functional`,
32+
`state_diff`, `python_behavior_contracts`, or `benchmarks`; each one must emit an
33+
explicit ratio gate.
34+
35+
Crane must run `go test ./cmd/apm -run TestParityRealFunctionalAndStateDiffContracts -json`.
36+
That fixture-backed test executes the built Go `apm` binary in temporary
37+
projects and emits the existing completion gates directly:
38+
39+
```json
40+
{"crane":"gate","name":"functional","passing":N,"total":N}
41+
{"crane":"gate","name":"state_diff","passing":N,"total":N}
42+
```
43+
44+
Crane must also run the migration benchmark test. It executes fixture-backed
45+
Python-vs-Go benchmark workloads and emits:
46+
47+
```json
48+
{"crane":"gate","name":"benchmarks","passing":N,"total":N}
49+
```
50+
51+
A legacy boolean such as `{"name":"benchmarks","passed":true}` is not enough.
52+
The benchmark report must prove that every benchmarked command produced the
53+
expected real artifact or output evidence.
54+
55+
The completion criteria are command-specific:
56+
57+
| Command area | Required proof |
58+
| --- | --- |
59+
| `init` | Creates a real `apm.yml` manifest. |
60+
| `install` | Installs a local package, writes `apm.lock.yaml`, and materializes installed content under `apm_modules/` or target paths. |
61+
| `update` | Mutates the lockfile when a dependency changes and reports a real no-op when nothing changed. |
62+
| `compile` | Writes target artifacts such as `.github/copilot-instructions.md` from fixture project state. |
63+
| `pack` / `unpack` | Writes a non-empty distributable bundle and can extract it back into a temp project. |
64+
| `run` / `preview` / `list` | Reads project scripts, executes or previews the selected script, and reflects the actual manifest contents. |
65+
| `audit` / `policy` | Fails on planted hidden Unicode, missing lockfile state, or policy violations instead of always reporting success. |
66+
| `mcp` / `runtime` / `plugin` / `marketplace` | Persist real manifest or config changes, not just status text. |
67+
| `cache` | Removes cache entries while respecting the configured cache root. |
68+
| `prune` / `uninstall` | Removes only files owned by stale dependencies and proves the removed paths are gone. |
69+
| `deps` / `outdated` / `view` / `search` | Read lockfile, marketplace, or registry fixtures and report fixture-derived results. |
70+
| `self-update` / `experimental` / `config` | Persist or validate real configuration state where the Python command does. |
71+
72+
Each new command implementation should add or extend functional, state-diff, and
73+
benchmark fixture coverage before Crane can claim it moved the migration
74+
forward. Shims, dry-runs, mocks, and help-only assertions do not count as command
75+
completion.
2476

2577
## Cutover Trigger Conditions
2678

@@ -32,19 +84,23 @@ are true:
3284
`init`, `install`, `update`, `compile`, `pack`, `run`, `audit`,
3385
`policy`, `mcp`, `runtime`, `targets`, `list`, `view`, `cache`,
3486
`deps`, `marketplace`, `uninstall`, `prune`
35-
3. Python-vs-Go parity tests pass for all commands in the matrix
36-
4. The final Python-reference parity run has been frozen into a committed,
87+
3. `TestParityRealFunctionalAndStateDiffContracts` passes every fixture-backed
88+
real-command scenario and emits passing `functional` and `state_diff` gates
89+
4. Python-vs-Go parity tests pass for all commands in the matrix
90+
5. Migration benchmarks pass real fixture-backed command workloads and emit a
91+
passing counted `benchmarks` gate
92+
6. The final Python-reference parity run has been frozen into a committed,
3793
versioned golden fixture corpus. The corpus must include CLI inventory,
3894
help and usage output, error output, exit codes, generated files, lockfiles,
3995
config files, managed-file manifests, deterministic cache/config layout, and
4096
audit artifacts for the full command matrix.
41-
5. An all-Go golden replay passes against that corpus with no live Python
97+
7. An all-Go golden replay passes against that corpus with no live Python
4298
oracle. The replay must build `cmd/apm` and compare only the Go binary
4399
against checked-in fixtures.
44-
6. A no-Python-runtime check passes: `APM_PYTHON_BIN` is unset, the Python CLI
100+
8. A no-Python-runtime check passes: `APM_PYTHON_BIN` is unset, the Python CLI
45101
is hidden or unavailable to the replay, and the golden replay still passes.
46-
7. `go build ./cmd/apm` produces a single static binary
47-
8. CI passes on the crane PR branch (`crane/crane-migration-python-to-go-full-apm-cli-rewrite`)
102+
9. `go build ./cmd/apm` produces a single static binary
103+
10. CI passes on the crane PR branch (`crane/crane-migration-python-to-go-full-apm-cli-rewrite`)
48104

49105
## Cutover Steps
50106

0 commit comments

Comments
 (0)