chore: import upstream snapshot with attribution

This commit is contained in:
wehub-resource-sync
2026-07-13 12:09:03 +08:00
commit 2de7548470
2883 changed files with 374366 additions and 0 deletions
@@ -0,0 +1,210 @@
"""Code migration agent — deterministic recipes + agent-loop fallback scaffold.
The hard architectural primitive is the two-layer structure: deterministic
recipe pass first (fast, auditable, safe), then agent loop for remaining
failures with a hard budget and a failure-classification step that feeds a
taxonomy dashboard. This scaffold implements both layers and runs a
50-repo simulation with mixed outcomes.
Run: python main.py
"""
from __future__ import annotations
import random
from dataclasses import dataclass, field
# ---------------------------------------------------------------------------
# repo + failure taxonomy
# ---------------------------------------------------------------------------
FAILURE_CLASSES = [
"dep_upgrade_required",
"build_tool_drift",
"custom_annotation",
"test_flake",
"syntax_edge_case",
"budget_exhausted",
"coverage_regression",
]
@dataclass
class Repo:
name: str
loc: int
lang: str # "java" | "python"
hardness: float # 0..1
@dataclass
class Attempt:
repo: Repo
recipe_applied: int = 0
agent_turns: int = 0
cost_usd: float = 0.0
wall_min: float = 0.0
status: str = "pending" # "pass" | "fail"
failure_class: str | None = None
coverage_base: float = 80.0
coverage_final: float = 80.0
# ---------------------------------------------------------------------------
# deterministic recipe pass -- OpenRewrite / libcst stand-in
# ---------------------------------------------------------------------------
def run_recipes(repo: Repo) -> int:
"""Returns number of rewrites applied."""
base = 20 + int(repo.loc / 500)
return int(base * (1 - 0.2 * repo.hardness))
# ---------------------------------------------------------------------------
# agent loop -- classify failure, apply fix, retry; budget-aware
# ---------------------------------------------------------------------------
BUDGET_MIN = 30.0
BUDGET_USD = 8.0
BUDGET_TURNS = 20
def agent_loop(attempt: Attempt, rng: random.Random) -> None:
"""Simulates the plan-act loop until pass or budget exhaustion."""
# cost per turn drifts with hardness
per_turn_min = 2.8 + attempt.repo.hardness * 2.0
per_turn_usd = 0.45 + attempt.repo.hardness * 0.65
# probability of passing per turn depends on hardness (0.02-0.18)
turn_pass_p = max(0.02, 0.22 * (1 - attempt.repo.hardness * 0.95))
while True:
if attempt.agent_turns >= BUDGET_TURNS:
attempt.status = "fail"
attempt.failure_class = "budget_exhausted"
return
if attempt.wall_min >= BUDGET_MIN or attempt.cost_usd >= BUDGET_USD:
attempt.status = "fail"
attempt.failure_class = "budget_exhausted"
return
attempt.agent_turns += 1
attempt.wall_min += per_turn_min
attempt.cost_usd += per_turn_usd
if rng.random() < turn_pass_p:
# coverage check
delta = rng.gauss(0.0, 0.6)
attempt.coverage_final = attempt.coverage_base + delta
if attempt.coverage_final < attempt.coverage_base - 2.0:
attempt.status = "fail"
attempt.failure_class = "coverage_regression"
return
attempt.status = "pass"
return
# ---------------------------------------------------------------------------
# classification of stuck repos -- bucket into taxonomy
# ---------------------------------------------------------------------------
def classify_failure(rng: random.Random) -> str:
"""Stand-in for the agent's failure classifier. Real implementation
reads build logs and test output."""
weights = {
"dep_upgrade_required": 0.30,
"build_tool_drift": 0.20,
"custom_annotation": 0.18,
"test_flake": 0.15,
"syntax_edge_case": 0.17,
}
r = rng.random()
acc = 0.0
for cls, w in weights.items():
acc += w
if r <= acc:
return cls
return "syntax_edge_case"
# ---------------------------------------------------------------------------
# pipeline -- recipes then agent then PR/file outcome
# ---------------------------------------------------------------------------
def migrate(repo: Repo, rng: random.Random) -> Attempt:
attempt = Attempt(repo=repo)
attempt.recipe_applied = run_recipes(repo)
# easy repos often go straight to pass after recipes
straight_through_p = 0.55 * (1 - repo.hardness)
if rng.random() < straight_through_p:
delta = rng.gauss(0.0, 0.4)
attempt.coverage_final = attempt.coverage_base + delta
attempt.status = "pass"
attempt.wall_min = 3.0 + rng.random() * 4
attempt.cost_usd = 0.30
return attempt
# otherwise run the agent loop
agent_loop(attempt, rng)
if attempt.status == "fail" and attempt.failure_class == "budget_exhausted":
# classify root cause of why the budget was exhausted
if rng.random() < 0.75:
attempt.failure_class = classify_failure(rng)
return attempt
# ---------------------------------------------------------------------------
# 50-repo simulation
# ---------------------------------------------------------------------------
def synth_bench(rng: random.Random) -> list[Repo]:
bench: list[Repo] = []
for i in range(50):
lang = "java" if rng.random() < 0.6 else "python"
hardness = min(0.95, max(0.05, rng.gauss(0.65, 0.18)))
bench.append(Repo(name=f"repo-{i:02d}-{lang}",
loc=rng.randint(800, 40_000),
lang=lang,
hardness=hardness))
return bench
def main() -> None:
rng = random.Random(19)
bench = synth_bench(rng)
results: list[Attempt] = []
for repo in bench:
results.append(migrate(repo, rng))
passed = [a for a in results if a.status == "pass"]
failed = [a for a in results if a.status == "fail"]
print(f"=== migration-bench run (50 repos) ===")
print(f"passed : {len(passed):2d} ({len(passed) / 50:.1%})")
print(f"failed : {len(failed):2d}")
print("\nfailure taxonomy:")
taxonomy: dict[str, int] = {}
for a in failed:
taxonomy[a.failure_class or "unknown"] = taxonomy.get(a.failure_class or "unknown", 0) + 1
for cls, n in sorted(taxonomy.items(), key=lambda x: -x[1]):
print(f" {cls:24s} {n}")
if passed:
mean_cost = sum(a.cost_usd for a in passed) / len(passed)
mean_min = sum(a.wall_min for a in passed) / len(passed)
mean_turns = sum(a.agent_turns for a in passed) / len(passed)
mean_cov_delta = sum(a.coverage_final - a.coverage_base for a in passed) / len(passed)
print("\npass-set metrics:")
print(f" mean $/repo : ${mean_cost:.2f}")
print(f" mean wall min : {mean_min:.1f}")
print(f" mean agent turns: {mean_turns:.1f}")
print(f" mean cov delta : {mean_cov_delta:+.2f} points")
if __name__ == "__main__":
main()
@@ -0,0 +1,39 @@
# Code migration agent dashboard (TypeScript skeleton)
Multi-file TypeScript skeleton for the dashboard layer of the code migration
agent capstone. The agent (Python) runs in a sandbox; this server renders
progress for the operator.
## Layout
- `src/index.ts` — entry point, simulates ticks and optionally serves HTTP.
- `src/server.ts` — Hono routes for `/`, `/dashboard`, `/migrations`, `/migrations/:id`.
- `src/migrations.ts` — per-file state machine and seed data.
- `src/cost.ts` — turn count and dollar budget enforcement.
- `src/types.ts` — shared types.
- `tests/*.test.ts``node --test` style tests via `tsx`.
## Install
```bash
npm install
```
## Run
```bash
npm start # offline: simulate 40 ticks and print rollup
npm run serve # serve the HTML dashboard on PORT (default 8009)
```
## Verify
```bash
npm run typecheck
npm test
```
## Spec references
- Source lesson: `phases/19-capstone-projects/09-code-migration-agent/docs/en.md`
- Recipes: [OpenRewrite](https://docs.openrewrite.org), libcst.
@@ -0,0 +1,23 @@
{
"name": "code-migration-agent-dashboard",
"private": true,
"type": "module",
"version": "0.1.0",
"description": "Multi-file TypeScript skeleton for the code migration agent dashboard.",
"scripts": {
"start": "tsx src/index.ts",
"serve": "SERVE=1 tsx src/index.ts",
"typecheck": "tsc --noEmit",
"test": "tsx --test tests/*.test.ts"
},
"dependencies": {
"hono": "^4.6.0",
"@hono/node-server": "^1.13.0",
"zod": "^3.23.0"
},
"devDependencies": {
"@types/node": "^20.14.0",
"tsx": "^4.19.0",
"typescript": "^5.7.0"
}
}
@@ -0,0 +1,28 @@
import type { Migration } from "./types.js";
export const MAX_TURNS = 20;
export const BUDGET_USD = 8;
export function turnCostUsd(rng: () => number = Math.random): number {
return Number((0.06 + rng() * 0.18).toFixed(3));
}
export type BudgetVerdict = {
exhausted: boolean;
reason?: "turns" | "cost";
};
export function checkBudget(m: Migration): BudgetVerdict {
if (m.turns >= m.maxTurns) {
return { exhausted: true, reason: "turns" };
}
if (m.spentUsd >= m.budgetUsd) {
return { exhausted: true, reason: "cost" };
}
return { exhausted: false };
}
export function chargeTurn(m: Migration, rng: () => number = Math.random): void {
m.turns += 1;
m.spentUsd = Number((m.spentUsd + turnCostUsd(rng)).toFixed(3));
}
@@ -0,0 +1,55 @@
/**
* Code Migration Agent: dashboard skeleton entry point (TypeScript).
*
* Mirrors the dashboard layer from docs/en.md: agent runs in a sandbox; this
* server renders progress for the operator. Hono routes serve HTML root,
* /migrations, and /migrations/:id. State machine in migrations.ts; budget
* + cost in cost.ts; types in types.ts.
*
* Source: phases/19-capstone-projects/09-code-migration-agent/docs/en.md
* Recipe specs: https://docs.openrewrite.org and the libcst Python parser.
*/
import { serve } from "@hono/node-server";
import { buildApp } from "./server.js";
import { defaultSeed, rolledUpStats, tickAll } from "./migrations.js";
function summarise(migrations: ReturnType<typeof defaultSeed>): void {
const stats = rolledUpStats(migrations);
console.log("[dashboard] migrations seeded:", migrations.length);
for (const m of migrations) {
const passed = m.files.filter((f) => f.status === "passed").length;
console.log(
`[dashboard] ${m.repo} ${m.sourceRuntime}->${m.targetRuntime} ` +
`state=${m.state} files=${passed}/${m.files.length} ` +
`turns=${m.turns}/${m.maxTurns} cost=$${m.spentUsd.toFixed(2)}`,
);
}
console.log("[dashboard] roll-up:", stats);
}
export function runDemoTicks(rounds: number): ReturnType<typeof defaultSeed> {
const migrations = defaultSeed();
for (let i = 0; i < rounds; i++) tickAll(migrations);
return migrations;
}
function main(): void {
console.log("[dashboard] simulating 40 ticks of agent progress...");
const migrations = runDemoTicks(40);
summarise(migrations);
if (process.env["SERVE"] === "1") {
const port = Number(process.env["PORT"] ?? 8009);
const app = buildApp(migrations);
serve({ fetch: app.fetch, port }, (info) => {
console.log(`[dashboard] serving on http://localhost:${info.port}`);
});
setInterval(() => tickAll(migrations), 750).unref();
} else {
console.log(
"[dashboard] set SERVE=1 to start the HTTP dashboard on PORT (default 8009)",
);
}
}
main();
@@ -0,0 +1,145 @@
import { randomUUID } from "node:crypto";
import { BUDGET_USD, MAX_TURNS, chargeTurn, checkBudget } from "./cost.js";
import type {
FileDiff,
FileStatus,
Migration,
Recipe,
RolledUpStats,
} from "./types.js";
const STATE_ORDER: FileStatus[] = [
"queued",
"rewriting",
"building",
"passed",
];
export function fileDiff(
path: string,
recipe: Recipe,
status: FileStatus = "queued",
): FileDiff {
return {
path,
status,
recipe,
linesAdded: 0,
linesRemoved: 0,
testsTouched: 0,
};
}
export function seedMigration(
repo: string,
sourceRuntime: string,
targetRuntime: string,
files: FileDiff[],
): Migration {
return {
id: randomUUID(),
repo,
sourceRuntime,
targetRuntime,
startedAt: Date.now(),
budgetUsd: BUDGET_USD,
spentUsd: 0,
turns: 0,
maxTurns: MAX_TURNS,
files,
state: "running",
};
}
export function defaultSeed(): Migration[] {
return [
seedMigration("acme/payments-svc", "java-8", "java-17", [
fileDiff("pom.xml", "openrewrite"),
fileDiff("src/main/java/Payments.java", "openrewrite"),
fileDiff("src/main/java/Refunds.java", "openrewrite"),
fileDiff("src/test/java/PaymentsTest.java", "agent"),
]),
seedMigration("acme/billing-py", "python-2.7", "python-3.12", [
fileDiff("setup.py", "libcst"),
fileDiff("billing/core.py", "libcst"),
fileDiff("billing/dunning.py", "agent"),
fileDiff("tests/test_core.py", "libcst"),
]),
seedMigration("acme/checkout-svc", "java-8", "java-17", [
fileDiff("build.gradle", "openrewrite"),
fileDiff("src/main/java/Checkout.java", "openrewrite"),
fileDiff("src/main/java/Discount.java", "agent"),
]),
];
}
export function advanceFile(file: FileDiff, rng: () => number = Math.random): void {
if (file.status === "passed" || file.status === "failed") return;
const idx = STATE_ORDER.indexOf(file.status);
const next = STATE_ORDER[idx + 1];
if (!next) return;
file.status = next;
if (next === "rewriting") {
file.linesAdded = 4 + Math.floor(rng() * 24);
file.linesRemoved = 1 + Math.floor(rng() * 14);
}
if (next === "building" && rng() < 0.15) {
file.status = "failed";
file.lastError =
"compile error: cannot find symbol javax.annotation.Nullable";
}
if (next === "passed" && file.path.includes("test")) {
file.testsTouched = 2 + Math.floor(rng() * 6);
}
}
export function migrationDone(m: Migration): boolean {
return m.files.every((f) => f.status === "passed" || f.status === "failed");
}
export function tickOne(m: Migration, rng: () => number = Math.random): void {
if (m.state !== "running") return;
const inFlight = m.files.find(
(f) => f.status !== "passed" && f.status !== "failed",
);
if (!inFlight) {
m.state = m.files.some((f) => f.status === "failed") ? "failed" : "passed";
return;
}
advanceFile(inFlight, rng);
chargeTurn(m, rng);
const verdict = checkBudget(m);
if (verdict.exhausted) {
m.state = "failed";
return;
}
if (migrationDone(m)) {
m.state = m.files.some((f) => f.status === "failed") ? "failed" : "passed";
}
}
export function tickAll(migrations: Migration[], rng: () => number = Math.random): void {
for (const m of migrations) {
tickOne(m, rng);
}
}
export function rolledUpStats(migrations: Migration[]): RolledUpStats {
let running = 0;
let passed = 0;
let failed = 0;
let spent = 0;
for (const m of migrations) {
if (m.state === "running") running++;
if (m.state === "passed") passed++;
if (m.state === "failed") failed++;
spent += m.spentUsd;
}
return {
total: migrations.length,
running,
passed,
failed,
spentUsd: Number(spent.toFixed(3)),
};
}
@@ -0,0 +1,82 @@
import { Hono } from "hono";
import { rolledUpStats } from "./migrations.js";
import type { Migration } from "./types.js";
export function buildApp(migrations: Migration[]): Hono {
const app = new Hono();
app.get("/", (c) => c.html(renderDashboardHtml(migrations)));
app.get("/dashboard", (c) => c.html(renderDashboardHtml(migrations)));
app.get("/migrations", (c) =>
c.json({
stats: rolledUpStats(migrations),
migrations: migrations.map((m) => ({
id: m.id,
repo: m.repo,
state: m.state,
sourceRuntime: m.sourceRuntime,
targetRuntime: m.targetRuntime,
turns: m.turns,
spentUsd: m.spentUsd,
})),
}),
);
app.get("/migrations/:id", (c) => {
const id = c.req.param("id");
const m = migrations.find((x) => x.id === id);
if (!m) return c.json({ error: "not_found", id }, 404);
return c.json(m);
});
return app;
}
export function renderDashboardHtml(migrations: Migration[]): string {
const stats = rolledUpStats(migrations);
const rows = migrations
.map((m) => {
const passedFiles = m.files.filter((f) => f.status === "passed").length;
const pct = m.files.length === 0 ? 0 : Math.round((passedFiles / m.files.length) * 100);
return [
"<tr>",
`<td><a href="/migrations/${m.id}">${m.repo}</a></td>`,
`<td>${m.sourceRuntime} to ${m.targetRuntime}</td>`,
`<td>${m.state}</td>`,
`<td>${pct}%</td>`,
`<td>${m.turns}/${m.maxTurns}</td>`,
`<td>$${m.spentUsd.toFixed(2)}/$${m.budgetUsd}</td>`,
"</tr>",
].join("");
})
.join("\n");
return [
"<!doctype html>",
"<html><head><title>Code migration dashboard</title>",
"<style>",
"body{font-family:system-ui,sans-serif;margin:2rem;max-width:960px;}",
"table{border-collapse:collapse;width:100%;}",
"th,td{padding:.4rem .8rem;border-bottom:1px solid #ddd;text-align:left;}",
"th{background:#f3f3f3;}",
".stats{display:flex;gap:1.5rem;margin-bottom:1rem;}",
".stat{background:#fafafa;border:1px solid #ddd;padding:.6rem 1rem;border-radius:6px;}",
"</style></head><body>",
"<h1>Code migration dashboard</h1>",
"<div class='stats'>",
`<div class='stat'><b>${stats.total}</b> migrations</div>`,
`<div class='stat'>${stats.running} running</div>`,
`<div class='stat'>${stats.passed} passed</div>`,
`<div class='stat'>${stats.failed} failed</div>`,
`<div class='stat'>$${stats.spentUsd.toFixed(2)} spent</div>`,
"</div>",
"<table><thead><tr>",
"<th>repo</th><th>migration</th><th>state</th><th>progress</th><th>turns</th><th>cost</th>",
"</tr></thead><tbody>",
rows,
"</tbody></table>",
"<p><small>Auto-refreshes every 2s. Endpoints: /migrations, /migrations/:id.</small></p>",
"<script>setTimeout(()=>location.reload(),2000)</script>",
"</body></html>",
].join("\n");
}
@@ -0,0 +1,42 @@
export type FileStatus =
| "queued"
| "rewriting"
| "building"
| "passed"
| "failed";
export type Recipe = "openrewrite" | "libcst" | "agent";
export type FileDiff = {
path: string;
status: FileStatus;
recipe: Recipe;
linesAdded: number;
linesRemoved: number;
testsTouched: number;
lastError?: string;
};
export type MigrationState = "running" | "passed" | "failed" | "queued";
export type Migration = {
id: string;
repo: string;
sourceRuntime: string;
targetRuntime: string;
startedAt: number;
budgetUsd: number;
spentUsd: number;
turns: number;
maxTurns: number;
files: FileDiff[];
state: MigrationState;
};
export type RolledUpStats = {
total: number;
running: number;
passed: number;
failed: number;
spentUsd: number;
};
@@ -0,0 +1,41 @@
import { strict as assert } from "node:assert";
import { test } from "node:test";
import { BUDGET_USD, MAX_TURNS, chargeTurn, checkBudget } from "../src/cost.js";
import { defaultSeed } from "../src/migrations.js";
test("checkBudget returns clean when fresh", () => {
const m = defaultSeed()[0]!;
const v = checkBudget(m);
assert.equal(v.exhausted, false);
});
test("checkBudget flags turns exhausted", () => {
const m = defaultSeed()[0]!;
m.turns = MAX_TURNS;
const v = checkBudget(m);
assert.equal(v.exhausted, true);
assert.equal(v.reason, "turns");
});
test("checkBudget flags cost exhausted", () => {
const m = defaultSeed()[0]!;
m.spentUsd = BUDGET_USD;
const v = checkBudget(m);
assert.equal(v.exhausted, true);
assert.equal(v.reason, "cost");
});
test("chargeTurn increments turns and adds cost", () => {
const m = defaultSeed()[0]!;
chargeTurn(m, () => 0.5);
assert.equal(m.turns, 1);
assert.ok(m.spentUsd > 0);
assert.ok(m.spentUsd < BUDGET_USD);
});
test("chargeTurn upper bound stays inside budget per turn", () => {
const m = defaultSeed()[0]!;
for (let i = 0; i < MAX_TURNS; i++) chargeTurn(m, () => 1);
assert.equal(m.turns, MAX_TURNS);
assert.ok(m.spentUsd <= BUDGET_USD, `spent ${m.spentUsd} exceeds budget ${BUDGET_USD}`);
});
@@ -0,0 +1,59 @@
import { strict as assert } from "node:assert";
import { test } from "node:test";
import {
advanceFile,
defaultSeed,
fileDiff,
migrationDone,
rolledUpStats,
tickOne,
} from "../src/migrations.js";
test("seed produces three running migrations", () => {
const migrations = defaultSeed();
assert.equal(migrations.length, 3);
for (const m of migrations) {
assert.equal(m.state, "running");
assert.ok(m.files.length > 0);
}
});
test("advanceFile walks queued to rewriting to building to passed", () => {
const f = fileDiff("foo.java", "openrewrite");
const noFail = () => 0.99;
advanceFile(f, noFail);
assert.equal(f.status, "rewriting");
advanceFile(f, noFail);
assert.equal(f.status, "building");
advanceFile(f, noFail);
assert.equal(f.status, "passed");
});
test("advanceFile is a no-op on terminal states", () => {
const f = fileDiff("foo.java", "openrewrite");
f.status = "passed";
advanceFile(f);
assert.equal(f.status, "passed");
f.status = "failed";
advanceFile(f);
assert.equal(f.status, "failed");
});
test("tickOne can move a migration to passed when all files pass", () => {
const m = defaultSeed()[0]!;
const det = () => 0.99;
for (let i = 0; i < 200; i++) tickOne(m, det);
assert.equal(migrationDone(m), true);
assert.ok(m.state === "passed" || m.state === "failed");
});
test("rolledUpStats counts states correctly", () => {
const m = defaultSeed();
m[0]!.state = "passed";
m[1]!.state = "failed";
const stats = rolledUpStats(m);
assert.equal(stats.passed, 1);
assert.equal(stats.failed, 1);
assert.equal(stats.running, 1);
assert.equal(stats.total, 3);
});
@@ -0,0 +1,17 @@
{
"compilerOptions": {
"target": "ES2022",
"module": "NodeNext",
"moduleResolution": "NodeNext",
"strict": true,
"noUncheckedIndexedAccess": true,
"exactOptionalPropertyTypes": true,
"esModuleInterop": true,
"skipLibCheck": true,
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"types": ["node"]
},
"include": ["src/**/*.ts", "tests/**/*.ts"]
}