CodeRigor

LESSON

Migrate the JSON report CLI and prove parity

Preserve the original JavaScript CLI’s flags, physical line errors, overflow policy and deterministic output.

The capstone migrates the existing job-report tool while retaining its public behavior. Types describe arguments, validated records, groups and returned streams. Runtime checks still reject invalid JSON records, nonfinite durations and total overflow.

The local lab compiles the TypeScript implementation, runs actual Node processes, compares both stream/exit contracts with the JavaScript baseline and checks invalid type fixtures. A small JavaScript adapter owns stdin and process streams; the typed core remains a testable function.

Migrate the JSON report CLI and prove parity
start / 3
Preserve the original JavaScript CLI’s flags, physical line errors, overflow policy and deterministic output.
Migrate the JSON report CLI and prove parity
Preserve the original JavaScript CLI’s flags, physical line errors, overflow policy and deterministic output.

Preserve flag precedence: --help succeeds even if input is malformed, and an unknown flag fails before parsing. Blank physical lines are ignored as records but still increment later error locations. Validate every record before applying --failed, so an invalid unselected record is not hidden.

Keep the first failure stable. A selected group overflowing on line2 must report line2 even if line3 is malformed. Process and accumulate in physical order, then sort successful output only after every required record has been handled.

TypeScript
export type JobState = "ok" | "failed";
export interface Job { service: string; state: JobState; duration: number; }
export interface Group { service: string; count: number; failed: number; totalDuration: number; }
export interface CLIResult { code: number; stdout: string; stderr: string; }

export function parseJob(value: unknown): Job {
    if (value === null || typeof value !== "object" || Array.isArray(value)) throw new TypeError("invalid record");
    if (!("service" in value) || typeof value.service !== "string" || value.service.trim() === "") throw new TypeError("invalid service");
    if (!("state" in value) || (value.state !== "ok" && value.state !== "failed")) throw new TypeError("invalid state");
    if (!("duration" in value) || typeof value.duration !== "number" || !Number.isFinite(value.duration) || value.duration < 0) throw new TypeError("invalid duration");
    return { service: value.service.trim(), state: value.state, duration: value.duration };
}

export function mapValues<T, U>(values: readonly T[], transform: (value: T, index: number) => U): U[] {
    const result: U[] = [];
    for (const [index, value] of values.entries()) {
        result.push(transform(value, index));
    }
    return result;
}

const usage = "usage: job-report [--failed]\n";
export function runCLI(args: readonly string[], input: string): CLIResult {
    if (args.length === 1 && args[0] === "--help") return { code: 0, stdout: usage, stderr: "" };
    if (!(args.length === 0 || (args.length === 1 && args[0] === "--failed"))) return { code: 2, stdout: "", stderr: usage };
    const groups = new Map<string, Group>();
    for (const [index, line] of input.split(/\r?\n/).entries()) {
        if (line.trim() === "") continue;
        let row: Job;
        try {
            const decoded: unknown = JSON.parse(line);
            row = parseJob(decoded);
        } catch {
            return { code: 1, stdout: "", stderr: `invalid record at line ${index + 1}\n` };
        }
        if (args[0] === "--failed" && row.state !== "failed") continue;
        const previous = groups.get(row.service) ?? { service: row.service, count: 0, failed: 0, totalDuration: 0 };
        const totalDuration = previous.totalDuration + row.duration;
        if (!Number.isFinite(totalDuration)) return { code: 1, stdout: "", stderr: `invalid record at line ${index + 1}\n` };
        groups.set(row.service, { service: row.service, count: previous.count + 1, failed: previous.failed + (row.state === "failed" ? 1 : 0), totalDuration });
    }
    const ordered = [...groups.values()].sort((a,b) => a.service < b.service ? -1 : a.service > b.service ? 1 : 0);
    const result = mapValues(ordered, row => ({ service: row.service, count: row.count, failed: row.failed, totalDuration: row.totalDuration }));
    return { code: 0, stdout: JSON.stringify(result) + "\n", stderr: "" };
}
const input = '{"service":"api","state":"failed","duration":2}\n';
const result = runCLI(["--failed"], input);
console.log(result.code);
console.log(result.stdout.trim());

The typed core returns a CLIResult, and the local adapter writes stdout/stderr and applies its exit code. No partial success output appears before validation finishes. The parity matrix includes help, bad flags, empty input, selected failures, malformed physical lines and numeric overflow.

This migration is complete for the existing small tool. It does not add network fetching, database access or a production service. Extend the type design after behavioral parity is established; the Boot.dev continuation offers broader language practice.

Keep the external-data boundary
start / 3
Type relationships are checked before execution. JSON fields still need actual runtime checks.
Keep the external-data boundary
Type relationships are checked before execution. JSON fields still need actual runtime checks.
TypeScript
export type JobState = "ok" | "failed";
export interface Job { service: string; state: JobState; duration: number; }
export interface Group { service: string; count: number; failed: number; totalDuration: number; }
export interface CLIResult { code: number; stdout: string; stderr: string; }

export function parseJob(value: unknown): Job {
    if (value === null || typeof value !== "object" || Array.isArray(value)) throw new TypeError("invalid record");
    if (!("service" in value) || typeof value.service !== "string" || value.service.trim() === "") throw new TypeError("invalid service");
    if (!("state" in value) || (value.state !== "ok" && value.state !== "failed")) throw new TypeError("invalid state");
    if (!("duration" in value) || typeof value.duration !== "number" || !Number.isFinite(value.duration) || value.duration < 0) throw new TypeError("invalid duration");
    return { service: value.service.trim(), state: value.state, duration: value.duration };
}

export function mapValues<T, U>(values: readonly T[], transform: (value: T, index: number) => U): U[] {
    const result: U[] = [];
    for (const [index, value] of values.entries()) {
        result.push(transform(value, index));
    }
    return result;
}

const usage = "usage: job-report [--failed]\n";
export function runCLI(args: readonly string[], input: string): CLIResult {
    if (args.length === 1 && args[0] === "--help") return { code: 0, stdout: usage, stderr: "" };
    if (!(args.length === 0 || (args.length === 1 && args[0] === "--failed"))) return { code: 2, stdout: "", stderr: usage };
    const groups = new Map<string, Group>();
    for (const [index, line] of input.split(/\r?\n/).entries()) {
        if (line.trim() === "") continue;
        let row: Job;
        try {
            const decoded: unknown = JSON.parse(line);
            row = parseJob(decoded);
        } catch {
            return { code: 1, stdout: "", stderr: `invalid record at line ${index + 1}\n` };
        }
        if (args[0] === "--failed" && row.state !== "failed") continue;
        const previous = groups.get(row.service) ?? { service: row.service, count: 0, failed: 0, totalDuration: 0 };
        const totalDuration = previous.totalDuration + row.duration;
        if (!Number.isFinite(totalDuration)) return { code: 1, stdout: "", stderr: `invalid record at line ${index + 1}\n` };
        groups.set(row.service, { service: row.service, count: previous.count + 1, failed: previous.failed + (row.state === "failed" ? 1 : 0), totalDuration });
    }
    const ordered = [...groups.values()].sort((a,b) => a.service < b.service ? -1 : a.service > b.service ? 1 : 0);
    const result = mapValues(ordered, row => ({ service: row.service, count: row.count, failed: row.failed, totalDuration: row.totalDuration }));
    return { code: 0, stdout: JSON.stringify(result) + "\n", stderr: "" };
}
const result = runCLI(["--failed"], '\n{"service":"api","state":"ok","duration":"bad"}');
console.log(result.code);
console.log(result.stderr.trim());

Run npm ci, npm test and the CLI commands in the downloadable lab. Inspect the actual compiler diagnostics and emitted JavaScript alongside the parity report. Change a type fixture to remove its expected error and confirm the negative checker fails.

Cleanup is limited to the extracted lab’s node_modules, generated build/receipts and files you created. No account, credential, network service or production database is used. The hosted exercises keep the existing offline execution boundary.

Download the complete lab. The archive includes runnable code, tests, fixtures and a README.

Pinned compiler 6.0.3 · Actual observed results

Types reject source. Runtime checks reject data.

These six invalid programs were checked by the real compiler under the lab’s strict settings. Choose one to inspect its source and actual diagnostic. This is separate from the migrated CLI’s runtime behavior.

TypeScript sourcestrict checksJavaScript emission

The rejected source

TypeScript
import {totalDuration} from "../src/core";
totalDuration(["2"]);

Actual TS2322 diagnostic

Text
negative/contracts.ts(2,16): error TS2322: Type 'string' is not assignable to type 'number'.

Twelve baseline parity cases · Twenty-four actual Node processes

Choose a case to inspect the exact typed CLI result that matched the original JavaScript process, including stdout, stderr and exit status. Compiler success alone does not establish this parity.

job-report · exit 0

JSON
{
  "code": 0,
  "stdout": "[{\"service\":\"a\",\"count\":1,\"failed\":1,\"totalDuration\":0},{\"service\":\"z\",\"count\":2,\"failed\":1,\"totalDuration\":5}]\n",
  "stderr": ""
}
Inspect actual emitted JavaScript from the complete typed core
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.totalDuration = totalDuration;
exports.displayRows = displayRows;
exports.decideState = decideState;
exports.describeState = describeState;
exports.parseJob = parseJob;
exports.mapValues = mapValues;
exports.runCLI = runCLI;
function totalDuration(values) {
    let total = 0;
    for (const value of values) {
        if (!Number.isFinite(value) || value < 0)
            throw new RangeError("finite nonnegative duration required");
        total += value;
        if (!Number.isFinite(total))
            throw new RangeError("duration total overflow");
    }
    return total;
}
function displayRows(jobs) {
    return jobs.map(job => ({ service: job.service, label: job.label ?? job.service }));
}
function decideState(value) {
    if (value === "ok" || value === "failed")
        return { kind: "valid", state: value };
    return { kind: "invalid", reason: "state must be ok or failed" };
}
function describeState(decision) {
    switch (decision.kind) {
        case "valid": return "state: " + decision.state;
        case "invalid": return decision.reason;
        default: {
            const unreachable = decision;
            return unreachable;
        }
    }
}
function parseJob(value) {
    if (value === null || typeof value !== "object" || Array.isArray(value))
        throw new TypeError("invalid record");
    if (!("service" in value) || typeof value.service !== "string" || value.service.trim() === "")
        throw new TypeError("invalid service");
    if (!("state" in value) || (value.state !== "ok" && value.state !== "failed"))
        throw new TypeError("invalid state");
    if (!("duration" in value) || typeof value.duration !== "number" || !Number.isFinite(value.duration) || value.duration < 0)
        throw new TypeError("invalid duration");
    return { service: value.service.trim(), state: value.state, duration: value.duration };
}
function mapValues(values, transform) {
    const result = [];
    for (const [index, value] of values.entries()) {
        result.push(transform(value, index));
    }
    return result;
}
const usage = "usage: job-report [--failed]\n";
function runCLI(args, input) {
    if (args.length === 1 && args[0] === "--help")
        return { code: 0, stdout: usage, stderr: "" };
    if (!(args.length === 0 || (args.length === 1 && args[0] === "--failed")))
        return { code: 2, stdout: "", stderr: usage };
    const groups = new Map();
    for (const [index, line] of input.split(/\r?\n/).entries()) {
        if (line.trim() === "")
            continue;
        let row;
        try {
            const decoded = JSON.parse(line);
            row = parseJob(decoded);
        }
        catch {
            return { code: 1, stdout: "", stderr: `invalid record at line ${index + 1}\n` };
        }
        if (args[0] === "--failed" && row.state !== "failed")
            continue;
        const previous = groups.get(row.service) ?? { service: row.service, count: 0, failed: 0, totalDuration: 0 };
        const totalDuration = previous.totalDuration + row.duration;
        if (!Number.isFinite(totalDuration))
            return { code: 1, stdout: "", stderr: `invalid record at line ${index + 1}\n` };
        groups.set(row.service, { service: row.service, count: previous.count + 1, failed: previous.failed + (row.state === "failed" ? 1 : 0), totalDuration });
    }
    const ordered = [...groups.values()].sort((a, b) => a.service < b.service ? -1 : a.service > b.service ? 1 : 0);
    const result = mapValues(ordered, row => ({ service: row.service, count: row.count, failed: row.failed, totalDuration: row.totalDuration }));
    return { code: 0, stdout: JSON.stringify(result) + "\n", stderr: "" };
}
Download the source, locked compiler setup, diagnostics and CLI checks →

Watch & read

Free lectures, talks and readings that cover this skill from another angle. All resources →