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,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"]
}