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
6 changes: 3 additions & 3 deletions .bandwidth/catalog-info.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,10 @@ apiVersion: backstage.io/v1alpha1
kind: Location
metadata:
schemaVersion: v1.0.0
name: bw-voice-adapter-location
description: Links to additional entities in the bw-voice-adapter repository.
name: migration-toolkit-location
description: Links to additional entities in the migration-toolkit repository.
annotations:
github.com/project-slug: Bandwidth/bw-voice-adapter
github.com/project-slug: Bandwidth/migration-toolkit
spec:
targets:
- ./component.yaml
Expand Down
6 changes: 3 additions & 3 deletions .bandwidth/component.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,10 @@ apiVersion: backstage.io/v1alpha1
kind: Component
metadata:
schemaVersion: v1.0.0
name: bw-voice-adapter
description: voice adapter
name: migration-toolkit
description: Developer Migration Toolkit — tools to ease Twilio→Bandwidth voice migration
annotations:
github.com/project-slug: Bandwidth/bw-voice-adapter
github.com/project-slug: Bandwidth/migration-toolkit
bandwidth.com/languages: Node.js
organization: BW
costCenter:
Expand Down
10 changes: 5 additions & 5 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ An agent runs `band` to provision, then configures and runs this adapter.

## What an agent can and cannot do

**Can do unattended:** preflight analysis, provisioning on an *existing*
**Can do unattended:** compatibility-check analysis, provisioning on an *existing*
Bandwidth account via `band`, adapter configuration, and the readiness check.

**🧍 Human required** (flagged inline below): creating a brand-new Bandwidth
Expand All @@ -27,11 +27,11 @@ host, and final by-ear call verification.

## Runbook

### Phase 1 — Preflight
### Phase 1 — Compatibility Check
```bash
npm install
npm run preflight -- <path-to-customer-twilio-app> # complexity + per-file verdict
npm run generate -- <in-dir> <out-dir> # writes out/bxml/*, out/MIGRATION.md, out/coverage.json
npm run compatibility-check -- <path-to-customer-twilio-app> # complexity + per-file verdict
npm run bxml-generator -- <in-dir> <out-dir> # writes out/bxml/*, out/MIGRATION.md, out/coverage.json
```
Read `out/coverage.json` for the machine-readable verdict. If a source file uses
the Twilio SDK to build TwiML at runtime, it is a **dynamic source** — there is no
Expand Down Expand Up @@ -123,7 +123,7 @@ Translation is a fixed rulebook (`src/matrix/twilio-voice.json`), not a guess.
there is no static markup to transpile. Run the adapter with
`ADAPTER_CAPTURE_DIR=<dir>` and place a few test calls; each customer TwiML
response is written there verbatim (content-addressed, deduped). Then
`npm run generate -- <dir> <out>` produces standalone BXML for the paths those
`npm run bxml-generator -- <dir> <out>` produces standalone BXML for the paths those
calls exercised. Capture only covers exercised paths — branches you never dial
won't appear, so drive every flow you care about (or port the rest by hand).

Expand Down
4 changes: 2 additions & 2 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
# Contributing

Thanks for helping make the adapter better. Issues and PRs are the main way it improves, and they're genuinely welcome.
Thanks for helping make the Migration Toolkit better. Issues and PRs are the main way it improves, and they're genuinely welcome.

## How this project is supported

bw-voice-adapter is best-effort and community-supported — not a fully supported Bandwidth product. It's proven on real calls, but there's no team on it full-time, so response times vary and prioritization isn't guaranteed. If something's blocking you, the fastest path is usually a PR you drive yourself. For SLA-backed support on a production migration, talk to your Bandwidth account team.
The Migration Toolkit is best-effort and community-supported — not a fully supported Bandwidth product. It's proven on real calls, but there's no team on it full-time, so response times vary and prioritization isn't guaranteed. If something's blocking you, the fastest path is usually a PR you drive yourself. For SLA-backed support on a production migration, talk to your Bandwidth account team.

## How to report a problem

Expand Down
172 changes: 103 additions & 69 deletions README.md

Large diffs are not rendered by default.

12 changes: 6 additions & 6 deletions docs/demo.md
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
# Demo walkthrough

## 1. Pre-flight report (no credentials needed)
## 1. Compatibility Check (no credentials needed)

```bash
npm run preflight -- examples/sample-twilio-app
npm run compatibility-check -- examples/sample-twilio-app
```

Matrix-driven migration report for an unmodified Twilio app: complexity score,
Expand All @@ -17,7 +17,7 @@ Terminal A — the "customer's" untouched Twilio app:
cd examples/sample-twilio-app && npm install && npm start
```

Terminal B — the adapter:
Terminal B — the translator:

```bash
ADAPTER_ACCOUNT_SID=AC123 ADAPTER_AUTH_TOKEN=demo \
Expand Down Expand Up @@ -62,7 +62,7 @@ adapter URL (`/bw/initiate`), and a public HTTPS tunnel (ngrok) or small host.
(A SIP peer is only needed on the legacy platform; the default Universal
Platform path uses a VCP instead — see `AGENTS.md` Phase 2.) Call the BW number,
walk the IVR by ear. Then exercise outbound via the REST facade with the real
`twilio` SDK pointed at the adapter base URL.
`twilio` SDK pointed at the translator base URL.

P0 exit criteria for the live milestone:
1. Inbound IVR (Say/Gather/Transfer) works on a real phone call.
Expand All @@ -79,7 +79,7 @@ CLI; see the Phase 2 runbook in [`AGENTS.md`](../AGENTS.md).
Add a stand-in for the customer's callback receiver:

```bash
# Terminal D — prints whatever the adapter posts back
# Terminal D — prints whatever the translator posts back
node -e "require('http').createServer((q,s)=>{let b='';q.on('data',d=>b+=d);q.on('end',()=>{console.log('\n['+q.url+'] '+b);s.end('ok')})}).listen(4001,()=>console.log('catcher :4001'))"
```

Expand All @@ -105,7 +105,7 @@ Expected (Terminal D): a Twilio `recordingStatusCallback` payload —
### 4b. Status callback (live call)

The `StatusCallback` URL is captured on the outbound `calls.create` path, so this
is shown on a live call rather than locally. Point the `twilio` SDK at the adapter:
is shown on a live call rather than locally. Point the `twilio` SDK at the translator:

```js
client.calls.create({
Expand Down
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 3 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"name": "bw-voice-adapter",
"name": "migration-toolkit",
"version": "0.1.0",
"private": true,
"type": "module",
Expand All @@ -8,8 +8,8 @@
"typecheck": "tsc --noEmit",
"start": "tsx src/server/index.ts",
"doctor": "tsx src/server/doctor.ts",
"preflight": "tsx src/preflight/cli.ts",
"generate": "tsx src/generate/cli.ts",
"compatibility-check": "tsx src/compatibility-check/cli.ts",
"bxml-generator": "tsx src/bxml-generator/cli.ts",
"bench": "tsx scripts/bench-translate.ts",
"bench:overhead": "tsx scripts/bench-adapter-overhead.ts",
"latency:report": "tsx scripts/latency-report.ts",
Expand Down
2 changes: 1 addition & 1 deletion scripts/build-playground.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Builds the self-contained Migration Preflight artifact: web/ sources + the
// Builds the self-contained Migration Compatibility Check artifact: web/ sources + the
// real translation engine + DM Sans fonts, all inlined into a single
// dist/playground.html a salesperson can double-click. No server, no network.

Expand Down
2 changes: 1 addition & 1 deletion src/generate/cli.ts → src/bxml-generator/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ function walk(dir: string, out: string[] = []): string[] {
const input = process.argv[2];
const outdir = process.argv[3];
if (!input || !outdir) {
console.error("Usage: npm run generate -- <path-to-repo-or-file> <outdir>");
console.error("Usage: npm run bxml-generator -- <path-to-repo-or-file> <outdir>");
process.exit(2);
}

Expand Down
File renamed without changes.
2 changes: 1 addition & 1 deletion src/generate/ingest.ts → src/bxml-generator/ingest.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { analyzeSource } from "../preflight/analyze.js";
import { analyzeSource } from "../compatibility-check/analyze.js";

// A single translatable TwiML document pulled out of the customer's app:
// a standalone .xml file, a <Response> string embedded in source, or a TwiML
Expand Down
2 changes: 1 addition & 1 deletion src/generate/report.ts → src/bxml-generator/report.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ function uniqueUrls(urls: PreservedUrl[]): PreservedUrl[] {
return [...seen.values()];
}

/** Plain-English migration report, mirroring the pre-flight report's tone. */
/** Plain-English migration report, mirroring the Compatibility Check report's tone. */
export function renderMigration(result: GenerateResult): string {
const { docs, dynamicSources } = result;
const clean = docs.filter((d) => d.findings.length === 0).length;
Expand Down
File renamed without changes.
2 changes: 1 addition & 1 deletion src/preflight/cli.ts → src/compatibility-check/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ function walk(dir: string, out: string[] = []): string[] {

const target = process.argv[2];
if (!target) {
console.error("Usage: npm run preflight -- <path-to-repo-or-file>");
console.error("Usage: npm run compatibility-check -- <path-to-repo-or-file>");
process.exit(2);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ export function renderReport(analyses: FileAnalysis[]): string {
const relevant = analyses.filter((a) => a.verbs.length > 0);
const score = complexityScore(relevant);
const lines: string[] = [
"# Twilio → Bandwidth migration pre-flight report",
"# Twilio → Bandwidth migration — Compatibility Check",
"",
`**Migration complexity: ${score}/10** (1 + warnings + 3×blockers, capped at 10)`,
"",
Expand All @@ -23,7 +23,7 @@ export function renderReport(analyses: FileAnalysis[]): string {
const errors = a.findings.filter((f) => f.severity === "error");
const warnings = a.findings.filter((f) => f.severity === "warning");
if (errors.length === 0 && warnings.length === 0)
lines.push("✅ Runs through the adapter as-is.", "");
lines.push("✅ Runs through the translator as-is.", "");
if (errors.length) {
lines.push("### 🛑 Blockers", "");
for (const f of errors)
Expand Down
4 changes: 2 additions & 2 deletions src/server/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ export interface AdapterConfig {
/** Basic-auth credentials Bandwidth presents on inbound webhooks (must match the app's CallbackCreds). */
webhookUser: string;
webhookPassword: string;
/** Opt-in dir to persist each customer TwiML response for later `generate`. Undefined → no capture. */
/** Opt-in dir to persist each customer TwiML response for the later BXML Generator. Undefined → no capture. */
captureDir?: string;
}

Expand Down Expand Up @@ -159,7 +159,7 @@ export function buildApp(config: AdapterConfig, deps: AdapterDeps): FastifyInsta
throw err;
}
// Capture the raw customer TwiML (URLs verbatim, pre-rewrite) before we
// translate it — this is exactly what `generate` ingests to produce
// translate it — this is exactly what the BXML Generator ingests to produce
// standalone BXML for the paths a test call exercised.
if (config.captureDir) {
try {
Expand Down
4 changes: 2 additions & 2 deletions src/server/capture.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,14 @@ import { join } from "node:path";

/**
* Persist a raw TwiML response the adapter fetched from the customer app, so a
* later `npm run generate` over the capture dir can turn the paths a test call
* later `npm run bxml-generator` over the capture dir can turn the paths a test call
* actually exercised into standalone BXML.
*
* The filename is a content hash — never request input — so identical responses
* dedupe to one file and untrusted input never participates in filesystem
* addressing (same guardrail as scripts/capture-server.mjs). We store the raw
* TwiML verbatim (customer URLs intact, no proxy rewrite), which is exactly what
* `generate` ingests.
* the BXML Generator ingests.
*
* Written private-by-default (dir 0700, file 0600): raw TwiML can carry callback
* URLs with embedded tokens, so captures are not world-readable. Synchronous I/O
Expand Down
2 changes: 1 addition & 1 deletion src/server/errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ export interface AdapterError {
// Adapter-private code range. Deliberately NOT in Twilio's registry — reusing a
// real Twilio code (e.g. 21610 = STOP/unsubscribed) with a twilio.com link would
// misdiagnose an adapter failure.
const DOCS = "https://github.com/Bandwidth/bw-voice-adapter/blob/main/AGENTS.md#errors";
const DOCS = "https://github.com/Bandwidth/migration-toolkit/blob/main/AGENTS.md#errors";

/** Operational errors the adapter itself raises (distinct from Twilio-API-compat errors). */
export const adapterErrors = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@ import { execFileSync } from "node:child_process";
import { readdirSync, readFileSync } from "node:fs";
import { fileURLToPath } from "node:url";
import { join } from "node:path";
import { ingest } from "../src/generate/ingest.js";
import { generateDocs } from "../src/generate/generate.js";
import { ingest } from "../src/bxml-generator/ingest.js";
import { generateDocs } from "../src/bxml-generator/generate.js";

// Conformance: generated standalone BXML must be accepted by Bandwidth's own
// CLI (`band bxml raw`). Skips (does not fail) when `band` is not on PATH, so
Expand Down
6 changes: 3 additions & 3 deletions test/generate.test.ts → test/bxml-generator.test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { describe, it, expect } from "vitest";
import { ingest, extractTwimlBlocks } from "../src/generate/ingest.js";
import { generateDoc, generateDocs, bxmlFileName } from "../src/generate/generate.js";
import { renderMigration, buildCoverage, type GenerateResult } from "../src/generate/report.js";
import { ingest, extractTwimlBlocks } from "../src/bxml-generator/ingest.js";
import { generateDoc, generateDocs, bxmlFileName } from "../src/bxml-generator/generate.js";
import { renderMigration, buildCoverage, type GenerateResult } from "../src/bxml-generator/report.js";

describe("ingest", () => {
it("treats a standalone .xml TwiML file as one doc", () => {
Expand Down
6 changes: 3 additions & 3 deletions test/preflight.test.ts → test/compatibility-check.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { describe, it, expect } from "vitest";
import { analyzeSource } from "../src/preflight/analyze.js";
import { renderReport, complexityScore } from "../src/preflight/report.js";
import { analyzeSource } from "../src/compatibility-check/analyze.js";
import { renderReport, complexityScore } from "../src/compatibility-check/report.js";

describe("analyzeSource", () => {
it("detects verbs in embedded TwiML strings", () => {
Expand Down Expand Up @@ -41,7 +41,7 @@ describe("report", () => {
`<Response><Say voice="alice">hi</Say><Enqueue>q</Enqueue></Response>`,
);
const md = renderReport([a]);
expect(md).toContain("# Twilio → Bandwidth migration pre-flight report");
expect(md).toContain("# Twilio → Bandwidth migration — Compatibility Check");
expect(md).toContain("Enqueue");
expect(md).toMatch(/complexity/i);
});
Expand Down
6 changes: 3 additions & 3 deletions web/README.md
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
# Migration Preflight playground
# Migration Compatibility Check playground

A single, self-contained HTML file a sales engineer can **double-click** and present to a prospect. Paste a customer's TwiML (or pick a curated example) and it shows — live — how much of that Twilio voice app runs on Bandwidth unchanged: a works-as-is / heads-up / blocker verdict, a migration-complexity score, the translated BXML, and a forwardable report.

No server, no install, no network. It works offline on a plane.

## Why it's trustworthy

The page runs the **real translation engine** (`src/translator`, `src/preflight`) and the real compatibility matrix (`src/matrix/twilio-voice.json`) in the browser — the exact same code the live adapter uses. It does not reimplement any translation or scoring logic. Because the same matrix drives the adapter and this page, the verdict shown here provably matches what happens in production. That invariant is the point; don't break it by hand-coding results into the UI.
The page runs the **real translation engine** (`src/translator`, `src/compatibility-check`) and the real compatibility matrix (`src/matrix/twilio-voice.json`) in the browser — the exact same code the live adapter uses. It does not reimplement any translation or scoring logic. Because the same matrix drives the adapter and this page, the verdict shown here provably matches what happens in production. That invariant is the point; don't break it by hand-coding results into the UI.

## Build & run

Expand Down Expand Up @@ -36,4 +36,4 @@ The visual design comes from a Bandwidth design-system mockup (DM Sans, primary

## Scope

Intentionally narrow: one screen, TwiML-document input only. It does **not** scan source code (that's the CLI `npm run preflight`), place live calls, or talk to Bandwidth. Those need a server and are out of scope for this demo tool.
Intentionally narrow: one screen, TwiML-document input only. It does **not** scan source code (that's the CLI `npm run compatibility-check`), place live calls, or talk to Bandwidth. Those need a server and are out of scope for this demo tool.
2 changes: 1 addition & 1 deletion web/app.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// DOM wiring for the Migration Preflight page. Keeps no logic of its own beyond
// DOM wiring for the Migration Compatibility Check page. Keeps no logic of its own beyond
// presentation — it calls buildView() (which runs the real engine) and paints
// the result. See view-model.ts for the honesty invariant.

Expand Down
6 changes: 3 additions & 3 deletions web/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Twilio → Bandwidth Migration Preflight</title>
<title>Twilio → Bandwidth Migration Compatibility Check</title>
<!-- build injects <style> (fonts + styles.css) here -->
</head>
<body>
Expand All @@ -16,9 +16,9 @@
<span class="pf-brand-mark">
<svg width="15" height="17" viewBox="0 0 35 40" fill="currentColor" aria-hidden="true"><path d="M27.17,10.08c-1.4-.3-1.56.43-1,1.76,3.17,7,.35,13.82-2.67,15.52a3.94,3.94,0,0,1-.08-.69c-.36-3.32-1.61-6.21-2.46-6.21-.68,0-.26,2.84-.44,5.35-1.12-.66-1.8-1.13-3-.92-1.71.3-4,3.93-4.07,3.41-.68-4.2,1-10.66,3.21-11.35.35-.14,1,.4,2.34.91,1.07.41.94-.66.45-1.06A5.21,5.21,0,0,0,14,16.41c-3.4,1.66-4.94,9.22-3.11,12.31a4.8,4.8,0,0,0,3.41,2.36c2.28.26,3-.22,4.81-2.47A2.14,2.14,0,0,0,20,29c-1.5,4.66-7.11,8.59-8.32,7.92-5.88-4-4.25-16.79-.1-28a64.52,64.52,0,0,0,2-6.23c.62-3-3.2-3.62-5.82-1.4C5.83,3,4,6.67,2.51,11c-4.75,13.9-2.73,25.6,5.77,28.15,9.78,2.92,14.43-3.18,15.15-9.31a10.86,10.86,0,0,0,1.8.09c3-.06,7.33-1.62,8.61-8.24C34.81,16.73,31.6,11,27.17,10.08Z"></path></svg>
</span>
<span class="pf-kicker">Migration Preflight</span>
<span class="pf-kicker">Migration Compatibility Check</span>
</div>
<h1 class="pf-title">Twilio&nbsp;&rarr;&nbsp;Bandwidth Migration Preflight</h1>
<h1 class="pf-title">Twilio&nbsp;&rarr;&nbsp;Bandwidth Migration Compatibility Check</h1>
<p class="pf-lede">
Paste the TwiML that drives your call flow and see, instantly, how much of it runs on
Bandwidth's network unchanged&nbsp;&mdash; and exactly what needs attention.
Expand Down
2 changes: 1 addition & 1 deletion web/styles.css
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
/* Migration Preflight — styling.
/* Migration Compatibility Check — styling.
* Brand tokens live in :root; swap them in one place to re-theme.
* Design system: Bandwidth (primary #076ea8, DM Sans). @font-face rules for
* DM Sans are injected by the build (base64) so the artifact stays self-contained. */
Expand Down
8 changes: 4 additions & 4 deletions web/view-model.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Pure view model for the Migration Preflight page.
// Pure view model for the Migration Compatibility Check page.
//
// This is the ONLY new logic in the web tool, and it is deliberately thin: it
// calls the real engine (analyzeSource / complexityScore / translateTwiml) and
Expand All @@ -8,8 +8,8 @@
// page, the verdict shown here provably matches what the adapter does in
// production. Keep this file DOM-free so it stays unit-testable.

import { analyzeSource } from "../src/preflight/analyze.js";
import { complexityScore, renderReport } from "../src/preflight/report.js";
import { analyzeSource } from "../src/compatibility-check/analyze.js";
import { complexityScore, renderReport } from "../src/compatibility-check/report.js";
import { translateTwiml } from "../src/translator/translate.js";
import { loadMatrix } from "../src/matrix/load.js";

Expand Down Expand Up @@ -42,7 +42,7 @@ export interface PreflightView {
headsUp: VerbCard[];
clean: VerbCard[];
bxml: string;
/** The same markdown migration report the CLI preflight produces (for "Copy report"). */
/** The same markdown migration report the CLI Compatibility Check produces (for "Copy report"). */
reportMarkdown: string;
}

Expand Down