feat: add /process page with tested delivery report model and sample
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,30 @@
|
||||
import * as React from "react";
|
||||
import { TerminalCard } from "@/src/components/site";
|
||||
import type { ProcessStep } from "./processSteps";
|
||||
|
||||
type ProcessStepCardProps = {
|
||||
readonly step: ProcessStep;
|
||||
readonly index: number;
|
||||
};
|
||||
|
||||
export const ProcessStepCard: React.FC<ProcessStepCardProps> = ({
|
||||
step,
|
||||
index,
|
||||
}) => (
|
||||
<li className="grid gap-6 border-t border-border py-8 md:grid-cols-2 md:gap-10">
|
||||
<div>
|
||||
<p className="!mb-2 font-mono text-xs uppercase tracking-widest text-accent">
|
||||
Step {index + 1}
|
||||
</p>
|
||||
<h3 className="!mb-3 !mt-0 text-xl font-semibold">{step.title}</h3>
|
||||
<p className="!mb-0 text-base leading-relaxed text-muted">{step.rule}</p>
|
||||
</div>
|
||||
<TerminalCard title={step.exampleLabel}>
|
||||
{step.example.map((line) => (
|
||||
<p key={line} className="!mb-0 whitespace-pre">
|
||||
{line}
|
||||
</p>
|
||||
))}
|
||||
</TerminalCard>
|
||||
</li>
|
||||
);
|
||||
@@ -0,0 +1,68 @@
|
||||
export type ProcessStep = {
|
||||
readonly id: string;
|
||||
readonly title: string;
|
||||
readonly rule: string;
|
||||
readonly exampleLabel: string;
|
||||
readonly example: readonly string[];
|
||||
};
|
||||
|
||||
export const PROCESS_STEPS: readonly ProcessStep[] = [
|
||||
{
|
||||
id: "spec",
|
||||
title: "Spec first",
|
||||
rule: "Behavior is written as concrete cases before any code: inputs, outputs, edge cases, error paths. Unclear business rules get one precise question to you, not a guess.",
|
||||
exampleLabel: "Availability parsing: the cases",
|
||||
example: [
|
||||
'"2026-02-30" -> rejected, not a real date',
|
||||
'"2028-02-29" -> accepted, leap day',
|
||||
"168 h per week -> accepted, the boundary",
|
||||
"169 h per week -> rejected",
|
||||
"-1 h per week -> rejected",
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "red",
|
||||
title: "Red",
|
||||
rule: "The cases run and fail for the right reason: the behavior is missing, not an import error or a typo. A bug fix starts with a failing test that reproduces the bug.",
|
||||
exampleLabel: "Availability parsing: first run",
|
||||
example: [
|
||||
"FAIL parses a leap day",
|
||||
" expected { ok: true }, received undefined",
|
||||
"9 failing, all because parseAvailability does not exist yet",
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "green",
|
||||
title: "Green",
|
||||
rule: "The simplest clean solution that makes the cases pass. Pure functions for the logic, side effects pushed to thin edges. Then refactor while the tests stay green.",
|
||||
exampleLabel: "Availability parsing: second run",
|
||||
example: [
|
||||
"9/9 pass",
|
||||
"date check: month length plus leap-year rule",
|
||||
"hours check: whole number from 0 to 168",
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "mutation",
|
||||
title: "Mutation check",
|
||||
rule: "A tool changes the code on purpose and reruns the tests. Every change must make a test fail. A surviving mutant means a weak assertion or dead code, and I fix one or the other.",
|
||||
exampleLabel: "Availability parsing: mutants",
|
||||
example: [
|
||||
"hours <= 168 -> hours < 168",
|
||||
" caught by the 168 h case",
|
||||
"day <= daysInMonth -> day < daysInMonth",
|
||||
" caught by the 2028-02-29 case",
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "report",
|
||||
title: "Delivery report",
|
||||
rule: "Every pull request carries the exact commands and their results: case counts, mutation result, coverage on changed lines, typecheck and lint. You can rerun every line.",
|
||||
exampleLabel: "Availability parsing: the report",
|
||||
example: [
|
||||
"spec 9 -> red 9 -> green 9/9",
|
||||
"mutation 14 caught, 0 survived",
|
||||
"see the full sample below",
|
||||
],
|
||||
},
|
||||
];
|
||||
@@ -0,0 +1,16 @@
|
||||
import type { DeliveryReport } from "@/src/domain/deliveryReport";
|
||||
|
||||
// Illustrative numbers for the availability parsing example. Not a real project.
|
||||
export const SAMPLE_DELIVERY_REPORT: DeliveryReport = {
|
||||
specCases: 9,
|
||||
redFailing: 9,
|
||||
greenPassing: 9,
|
||||
greenTotal: 9,
|
||||
mutantsTotal: 14,
|
||||
mutantsCaught: 14,
|
||||
mutantsSurvived: 0,
|
||||
changedLinesCovered: 23,
|
||||
changedLinesTotal: 23,
|
||||
typecheckErrors: 0,
|
||||
lintErrors: 0,
|
||||
};
|
||||
@@ -0,0 +1,120 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import {
|
||||
formatDeliveryReport,
|
||||
verdictOf,
|
||||
type DeliveryReport,
|
||||
} from "./deliveryReport";
|
||||
|
||||
const passing: DeliveryReport = {
|
||||
specCases: 9,
|
||||
redFailing: 9,
|
||||
greenPassing: 9,
|
||||
greenTotal: 9,
|
||||
mutantsTotal: 14,
|
||||
mutantsCaught: 14,
|
||||
mutantsSurvived: 0,
|
||||
changedLinesCovered: 23,
|
||||
changedLinesTotal: 23,
|
||||
typecheckErrors: 0,
|
||||
lintErrors: 0,
|
||||
};
|
||||
|
||||
describe("verdictOf", () => {
|
||||
it("is pass when every gate passes", () => {
|
||||
expect(verdictOf(passing)).toBe("pass");
|
||||
});
|
||||
|
||||
it.each<[string, Partial<DeliveryReport>]>([
|
||||
["no spec cases", { specCases: 0, redFailing: 0 }],
|
||||
["red does not match the written cases", { redFailing: 8 }],
|
||||
["a green test fails", { greenPassing: 8 }],
|
||||
["no green tests ran", { greenPassing: 0, greenTotal: 0 }],
|
||||
["a mutant survived", { mutantsCaught: 13, mutantsSurvived: 1 }],
|
||||
["no mutants were generated", { mutantsTotal: 0, mutantsCaught: 0 }],
|
||||
["caught plus survived does not add up", { mutantsCaught: 13 }],
|
||||
[
|
||||
"a survivor is reported although all mutants count as caught",
|
||||
{ mutantsSurvived: 1 },
|
||||
],
|
||||
["a changed line is uncovered", { changedLinesCovered: 22 }],
|
||||
[
|
||||
"no changed lines measured",
|
||||
{ changedLinesCovered: 0, changedLinesTotal: 0 },
|
||||
],
|
||||
["typecheck has errors", { typecheckErrors: 1 }],
|
||||
["lint has errors", { lintErrors: 2 }],
|
||||
])("is fail when %s", (_name, patch) => {
|
||||
expect(verdictOf({ ...passing, ...patch })).toBe("fail");
|
||||
});
|
||||
});
|
||||
|
||||
describe("formatDeliveryReport", () => {
|
||||
it("renders one line per gate and a verdict for a passing report", () => {
|
||||
expect(formatDeliveryReport(passing)).toEqual([
|
||||
"[pass] spec 9 cases written before code",
|
||||
"[pass] red 9 failing for the right reason",
|
||||
"[pass] green 9/9 pass",
|
||||
"[pass] mutation 14 mutants, 14 caught, 0 survived",
|
||||
"[pass] coverage changed lines 100% (23/23)",
|
||||
"[pass] typecheck clean",
|
||||
"[pass] lint clean",
|
||||
"verdict: pass",
|
||||
]);
|
||||
});
|
||||
|
||||
it("marks failing gates with their numbers", () => {
|
||||
const lines = formatDeliveryReport({
|
||||
...passing,
|
||||
redFailing: 8,
|
||||
greenPassing: 8,
|
||||
mutantsCaught: 13,
|
||||
mutantsSurvived: 1,
|
||||
changedLinesCovered: 22,
|
||||
typecheckErrors: 3,
|
||||
lintErrors: 1,
|
||||
});
|
||||
expect(lines).toEqual([
|
||||
"[pass] spec 9 cases written before code",
|
||||
"[fail] red 8 failing, expected 9",
|
||||
"[fail] green 8/9 pass",
|
||||
"[fail] mutation 14 mutants, 13 caught, 1 survived",
|
||||
"[fail] coverage changed lines 95.6% (22/23)",
|
||||
"[fail] typecheck 3 errors",
|
||||
"[fail] lint 1 error",
|
||||
"verdict: fail",
|
||||
]);
|
||||
});
|
||||
|
||||
it("never rounds a partial coverage up to 100", () => {
|
||||
const lines = formatDeliveryReport({
|
||||
...passing,
|
||||
changedLinesCovered: 9999,
|
||||
changedLinesTotal: 10000,
|
||||
});
|
||||
expect(lines).toContain(
|
||||
"[fail] coverage changed lines 99.9% (9999/10000)",
|
||||
);
|
||||
});
|
||||
|
||||
it("reports empty measurements as failures", () => {
|
||||
const lines = formatDeliveryReport({
|
||||
...passing,
|
||||
specCases: 0,
|
||||
redFailing: 0,
|
||||
greenPassing: 0,
|
||||
greenTotal: 0,
|
||||
mutantsTotal: 0,
|
||||
mutantsCaught: 0,
|
||||
changedLinesCovered: 0,
|
||||
changedLinesTotal: 0,
|
||||
});
|
||||
expect(lines.slice(0, 5)).toEqual([
|
||||
"[fail] spec no cases written",
|
||||
"[fail] red no cases written",
|
||||
"[fail] green 0/0 pass",
|
||||
"[fail] mutation no mutants generated",
|
||||
"[fail] coverage no changed lines measured",
|
||||
]);
|
||||
expect(lines.at(-1)).toBe("verdict: fail");
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,120 @@
|
||||
export type DeliveryReport = {
|
||||
readonly specCases: number;
|
||||
readonly redFailing: number;
|
||||
readonly greenPassing: number;
|
||||
readonly greenTotal: number;
|
||||
readonly mutantsTotal: number;
|
||||
readonly mutantsCaught: number;
|
||||
readonly mutantsSurvived: number;
|
||||
readonly changedLinesCovered: number;
|
||||
readonly changedLinesTotal: number;
|
||||
readonly typecheckErrors: number;
|
||||
readonly lintErrors: number;
|
||||
};
|
||||
|
||||
export type Verdict = "pass" | "fail";
|
||||
|
||||
type Gate = {
|
||||
readonly name: string;
|
||||
readonly pass: boolean;
|
||||
readonly detail: string;
|
||||
};
|
||||
|
||||
const NAME_COLUMN_WIDTH = 11;
|
||||
const PERCENT_SCALE = 1000;
|
||||
const PERCENT_DIVISOR = 10;
|
||||
|
||||
const plural = (count: number, singular: string): string =>
|
||||
`${count} ${singular}${count === 1 ? "" : "s"}`;
|
||||
|
||||
// Floor, never round: 99.96% must not be shown as 100%.
|
||||
const floorPercent = (part: number, whole: number): number =>
|
||||
Math.floor((part / whole) * PERCENT_SCALE) / PERCENT_DIVISOR;
|
||||
|
||||
const specGate = (r: DeliveryReport): Gate =>
|
||||
r.specCases > 0
|
||||
? {
|
||||
name: "spec",
|
||||
pass: true,
|
||||
detail: `${r.specCases} cases written before code`,
|
||||
}
|
||||
: { name: "spec", pass: false, detail: "no cases written" };
|
||||
|
||||
const redGate = (r: DeliveryReport): Gate => {
|
||||
if (r.specCases === 0) {
|
||||
return { name: "red", pass: false, detail: "no cases written" };
|
||||
}
|
||||
return r.redFailing === r.specCases
|
||||
? {
|
||||
name: "red",
|
||||
pass: true,
|
||||
detail: `${r.redFailing} failing for the right reason`,
|
||||
}
|
||||
: {
|
||||
name: "red",
|
||||
pass: false,
|
||||
detail: `${r.redFailing} failing, expected ${r.specCases}`,
|
||||
};
|
||||
};
|
||||
|
||||
const greenGate = (r: DeliveryReport): Gate => ({
|
||||
name: "green",
|
||||
pass: r.greenTotal > 0 && r.greenPassing === r.greenTotal,
|
||||
detail: `${r.greenPassing}/${r.greenTotal} pass`,
|
||||
});
|
||||
|
||||
const mutationGate = (r: DeliveryReport): Gate => {
|
||||
if (r.mutantsTotal === 0) {
|
||||
return { name: "mutation", pass: false, detail: "no mutants generated" };
|
||||
}
|
||||
return {
|
||||
name: "mutation",
|
||||
pass: r.mutantsSurvived === 0 && r.mutantsCaught === r.mutantsTotal,
|
||||
detail: `${r.mutantsTotal} mutants, ${r.mutantsCaught} caught, ${r.mutantsSurvived} survived`,
|
||||
};
|
||||
};
|
||||
|
||||
const coverageGate = (r: DeliveryReport): Gate => {
|
||||
if (r.changedLinesTotal === 0) {
|
||||
return {
|
||||
name: "coverage",
|
||||
pass: false,
|
||||
detail: "no changed lines measured",
|
||||
};
|
||||
}
|
||||
const percent = floorPercent(r.changedLinesCovered, r.changedLinesTotal);
|
||||
return {
|
||||
name: "coverage",
|
||||
pass: r.changedLinesCovered === r.changedLinesTotal,
|
||||
detail: `changed lines ${percent}% (${r.changedLinesCovered}/${r.changedLinesTotal})`,
|
||||
};
|
||||
};
|
||||
|
||||
const errorCountGate = (name: string, errors: number): Gate => ({
|
||||
name,
|
||||
pass: errors === 0,
|
||||
detail: errors === 0 ? "clean" : plural(errors, "error"),
|
||||
});
|
||||
|
||||
const gatesOf = (r: DeliveryReport): readonly Gate[] => [
|
||||
specGate(r),
|
||||
redGate(r),
|
||||
greenGate(r),
|
||||
mutationGate(r),
|
||||
coverageGate(r),
|
||||
errorCountGate("typecheck", r.typecheckErrors),
|
||||
errorCountGate("lint", r.lintErrors),
|
||||
];
|
||||
|
||||
export const verdictOf = (report: DeliveryReport): Verdict =>
|
||||
gatesOf(report).every((gate) => gate.pass) ? "pass" : "fail";
|
||||
|
||||
export const formatDeliveryReport = (
|
||||
report: DeliveryReport,
|
||||
): readonly string[] => [
|
||||
...gatesOf(report).map(
|
||||
(gate) =>
|
||||
`[${gate.pass ? "pass" : "fail"}] ${gate.name.padEnd(NAME_COLUMN_WIDTH)}${gate.detail}`,
|
||||
),
|
||||
`verdict: ${verdictOf(report)}`,
|
||||
];
|
||||
Reference in New Issue
Block a user