feat: add /notes with two engineering write-ups and diagrams

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-03 19:21:00 +02:00
co-authored by Claude Sonnet 5.5
parent cc0d9bd9c2
commit 4eb663f42a
11 changed files with 992 additions and 4 deletions
+91
View File
@@ -0,0 +1,91 @@
import type { Metadata } from "next";
import Link from "next/link";
import { notFound } from "next/navigation";
import { Container, Pill } from "@/src/components/site";
import { NOTES } from "@/src/content/notes";
import { SITE_NAME, SITE_URL } from "@/src/content/site";
import { findNoteBySlug } from "@/src/domain/notes";
type PageProps = { readonly params: Promise<{ readonly slug: string }> };
export const dynamicParams = false;
export function generateStaticParams() {
return NOTES.map((note) => ({ slug: note.slug }));
}
export async function generateMetadata({
params,
}: PageProps): Promise<Metadata> {
const found = findNoteBySlug(NOTES, (await params).slug);
if (!found.ok) return {};
const { title, summary, slug, publishedOn } = found.value;
const path = `/notes/${slug}`;
return {
title,
description: summary,
alternates: { canonical: path },
openGraph: {
type: "article",
title,
description: summary,
url: path,
publishedTime: publishedOn,
},
};
}
export default async function Page({ params }: PageProps) {
const found = findNoteBySlug(NOTES, (await params).slug);
if (!found.ok) notFound();
const { Body, title, summary, slug, publishedOn, readingMinutes, tags } =
found.value;
const jsonLd = {
"@context": "https://schema.org",
"@type": "Article",
headline: title,
description: summary,
datePublished: publishedOn,
url: `${SITE_URL}/notes/${slug}`,
author: { "@type": "Person", name: SITE_NAME, url: SITE_URL },
keywords: tags.join(", "),
};
return (
<Container className="py-16 md:py-24">
<article className="mx-auto max-w-[70ch]">
<script
type="application/ld+json"
dangerouslySetInnerHTML={{
__html: JSON.stringify(jsonLd).replace(/</g, "\\u003c"),
}}
/>
<Link
href="/notes"
className="font-mono text-xs uppercase tracking-widest text-accent"
>
&larr; Notes
</Link>
<header className="mt-6">
<h1 className="!mb-4 !text-3xl !leading-tight text-fg md:!text-5xl">
{title}
</h1>
<p className="!mb-4 font-mono text-xs text-muted">
<time dateTime={publishedOn}>{publishedOn}</time>
{" · "}
{readingMinutes} min read
</p>
<span className="flex flex-wrap gap-2">
{tags.map((tag) => (
<Pill key={tag}>{tag}</Pill>
))}
</span>
</header>
<div className="mt-10">
<Body />
</div>
</article>
</Container>
);
}
+37 -4
View File
@@ -1,8 +1,14 @@
import type { Metadata } from "next";
import { Container } from "@/src/components/site/Container";
import { SectionHeader } from "@/src/components/site/SectionHeader";
import Link from "next/link";
import { Container, Pill, SectionHeader } from "@/src/components/site";
import { NOTES } from "@/src/content/notes";
export const metadata: Metadata = { title: "Notes" };
export const metadata: Metadata = {
title: "Notes",
description:
"Evergreen engineering write-ups with diagrams: verification, architecture and testing.",
alternates: { canonical: "/notes" },
};
export default function Page() {
return (
@@ -11,8 +17,35 @@ export default function Page() {
as="h1"
kicker="/notes"
title="Notes"
lead="This page is coming soon."
lead="Evergreen engineering write-ups with diagrams. No client work, no hype."
/>
<ul className="mt-12 max-w-3xl divide-y divide-border border-y border-border">
{NOTES.map((note) => (
<li key={note.slug}>
<Link
href={`/notes/${note.slug}`}
className="group block py-8 hover:text-fg"
>
<p className="font-mono text-xs text-muted">
<time dateTime={note.publishedOn}>{note.publishedOn}</time>
{" · "}
{note.readingMinutes} min read
</p>
<h2 className="!mb-2 !mt-2 !text-xl !leading-snug text-fg group-hover:text-accent md:!text-2xl">
{note.title}
</h2>
<p className="!mb-4 text-base leading-relaxed text-muted">
{note.summary}
</p>
<span className="flex flex-wrap gap-2">
{note.tags.map((tag) => (
<Pill key={tag}>{tag}</Pill>
))}
</span>
</Link>
</li>
))}
</ul>
</Container>
);
}
@@ -0,0 +1,151 @@
import * as React from "react";
const MONO = "ui-monospace, monospace";
type Box = {
readonly y: number;
readonly height: number;
readonly title: string;
readonly lines: readonly string[];
readonly tag: string;
readonly variant: "shell" | "core" | "chain";
};
const BOXES: readonly Box[] = [
{
y: 10,
height: 66,
title: "Detectors, one per chain (7)",
lines: ["index borrower positions, read state"],
tag: "shell: I/O",
variant: "shell",
},
{
y: 120,
height: 110,
title: "Pure domain crate",
lines: [
"health factor",
"fixed-point arithmetic",
"closed-form optimum, checked",
"against an exact swap engine",
],
tag: "core: no I/O",
variant: "core",
},
{
y: 274,
height: 66,
title: "Submit shell",
lines: ["builds the transaction, sends it"],
tag: "shell: I/O",
variant: "shell",
},
{
y: 384,
height: 104,
title: "On-chain executor (Solidity)",
lines: [
"flash loan, liquidate, swap collateral,",
"repay. Reverts if the trade would",
"not be profitable.",
],
tag: "atomic",
variant: "chain",
},
];
const ARROWS = [
{ from: 76, label: "positions as data" },
{ from: 230, label: "decision as data" },
{ from: 340, label: "signed transaction" },
] as const;
const ARROW_LENGTH = 44;
const boxClass = (variant: Box["variant"]): string =>
variant === "core"
? "fill-surface stroke-accent"
: "fill-surface stroke-border-strong";
export const CoreShellDiagram: React.FC = () => (
<svg
viewBox="0 0 480 500"
role="img"
aria-labelledby="core-title core-desc"
className="h-auto w-full"
>
<title id="core-title">Pure core, thin shells, on-chain executor</title>
<desc id="core-desc">
Per-chain detector shells read state and pass positions as plain data to a
pure domain crate. The core returns a decision as data to a submit shell,
which sends a transaction to the on-chain executor. The executor takes a
flash loan, liquidates, swaps collateral and repays, and reverts if the
trade would not be profitable.
</desc>
{BOXES.map((box) => (
<g key={box.title}>
<rect
x={20}
y={box.y}
width={440}
height={box.height}
rx={6}
className={boxClass(box.variant)}
strokeWidth={box.variant === "core" ? 2 : 1.5}
strokeDasharray={box.variant === "chain" ? "6 4" : undefined}
/>
<text
x={36}
y={box.y + 24}
fontSize={15}
fontFamily={MONO}
className={box.variant === "core" ? "fill-accent" : "fill-current"}
>
{box.title}
</text>
{box.lines.map((line, index) => (
<text
key={line}
x={36}
y={box.y + 46 + index * 18}
fontSize={13}
fontFamily={MONO}
className="fill-muted"
>
{line}
</text>
))}
<text
x={444}
y={box.y + 24}
fontSize={12}
fontFamily={MONO}
textAnchor="end"
className="fill-muted"
>
{box.tag}
</text>
</g>
))}
{ARROWS.map((arrow) => (
<g key={arrow.label}>
<path
d={`M240 ${arrow.from + 2} v${ARROW_LENGTH - 8} m-5 -5 l5 5 l5 -5`}
className="stroke-border-strong"
fill="none"
strokeWidth={1.5}
/>
<text
x={254}
y={arrow.from + ARROW_LENGTH / 2 + 4}
fontSize={12}
fontFamily={MONO}
className="fill-muted"
>
{arrow.label}
</text>
</g>
))}
</svg>
);
@@ -0,0 +1,115 @@
import * as React from "react";
const GATES = [
{ title: "TDD gate", detail: "spec first, red before green" },
{
title: "Functional-style gate",
detail: "no methods, traits, &mut, unwrap",
},
{ title: "Mutation gate", detail: "zero survivors, proof ledger" },
{ title: "Coverage ratchet", detail: "fails when coverage drops" },
] as const;
const SPEEDS = [
{ label: "changed lines: dev loop", width: 150 },
{ label: "changed crates: pre-merge", width: 300 },
{ label: "everything: merge gate", width: 440 },
] as const;
const GATE_HEIGHT = 52;
const GATE_GAP = 20;
const GATE_TOP = 10;
const SPEEDS_TOP = 320;
const SPEED_ROW = 42;
export const GatePipelineDiagram: React.FC = () => (
<svg
viewBox="0 0 480 470"
role="img"
aria-labelledby="gate-title gate-desc"
className="h-auto w-full"
>
<title id="gate-title">The gate pipeline</title>
<desc id="gate-desc">
Four gates run in sequence: a TDD gate, a functional-style gate, a
mutation gate and a coverage ratchet. Below them, three scopes of
increasing size run the same gates: changed lines in the dev loop, changed
crates before merge, and everything at the merge gate.
</desc>
{GATES.map((gate, index) => {
const y = GATE_TOP + index * (GATE_HEIGHT + GATE_GAP);
return (
<g key={gate.title}>
<rect
x={20}
y={y}
width={440}
height={GATE_HEIGHT}
rx={6}
className="fill-surface stroke-accent"
strokeWidth={1.5}
/>
<text
x={36}
y={y + 22}
fontSize={15}
fontFamily="ui-monospace, monospace"
className="fill-accent"
>
{gate.title}
</text>
<text
x={36}
y={y + 42}
fontSize={13}
fontFamily="ui-monospace, monospace"
className="fill-muted"
>
{gate.detail}
</text>
{index < GATES.length - 1 ? (
<path
d={`M240 ${y + GATE_HEIGHT + 2} v${GATE_GAP - 8} m-5 -5 l5 5 l5 -5`}
className="stroke-border-strong"
fill="none"
strokeWidth={1.5}
/>
) : null}
</g>
);
})}
<text
x={20}
y={SPEEDS_TOP - 12}
fontSize={13}
fontFamily="ui-monospace, monospace"
className="fill-muted"
>
same gates, three speeds
</text>
{SPEEDS.map((speed, index) => {
const y = SPEEDS_TOP + index * SPEED_ROW;
return (
<g key={speed.label}>
<text
x={20}
y={y + 14}
fontSize={14}
fontFamily="ui-monospace, monospace"
className="fill-current"
>
{speed.label}
</text>
<rect
x={20}
y={y + 22}
width={speed.width}
height={6}
rx={3}
className="fill-accent"
/>
</g>
);
})}
</svg>
);
@@ -0,0 +1,39 @@
import * as React from "react";
type WithChildren = { readonly children: React.ReactNode };
export const NoteSection: React.FC<WithChildren> = ({ children }) => (
<h2 className="!mb-4 !mt-12 !text-2xl !leading-snug text-fg md:!text-3xl">
{children}
</h2>
);
export const NoteParagraph: React.FC<WithChildren> = ({ children }) => (
<p className="!mb-5 !text-base !leading-8 text-fg/85 md:!text-lg md:!leading-8">
{children}
</p>
);
export const NoteList: React.FC<WithChildren> = ({ children }) => (
<ul className="!mb-6 !ml-5 list-disc space-y-2 text-base leading-8 text-fg/85 marker:text-accent md:text-lg">
{children}
</ul>
);
export const NoteCode: React.FC<WithChildren> = ({ children }) => (
<code className="font-mono">{children}</code>
);
type NoteFigureProps = WithChildren & { readonly caption: string };
export const NoteFigure: React.FC<NoteFigureProps> = ({
children,
caption,
}) => (
<figure className="my-10 rounded-lg border border-border bg-surface p-5 md:p-8">
<div className="mx-auto max-w-md text-fg">{children}</div>
<figcaption className="mt-4 text-center font-mono text-xs text-muted">
{caption}
</figcaption>
</figure>
);
@@ -0,0 +1,121 @@
import * as React from "react";
import { CoreShellDiagram } from "@/src/components/notes/CoreShellDiagram";
import {
NoteFigure,
NoteList,
NoteParagraph,
NoteSection,
} from "@/src/components/notes/NoteProse";
import type { Note } from "./types";
const Body: React.FC = () => (
<>
<NoteParagraph>
I am building a liquidation system that runs across 7 chains. It is
written in Rust, with Solidity executors on chain. It is built to run
unattended, which changes how much you can rely on manual checking: almost
all of the confidence has to come from tests that run before the code
does. The architecture that makes this workable is a functional core with
imperative shells.
</NoteParagraph>
<NoteSection>The shape</NoteSection>
<NoteParagraph>
A detector per chain indexes borrower positions and computes health
factors. That work is split in two. The math lives in a pure domain crate:
health factor, fixed-point arithmetic, and a closed-form optimum that is
checked against an exact swap engine. The crate has no network, no clock,
no randomness and no global state. Everything arrives as arguments and
leaves as return values.
</NoteParagraph>
<NoteParagraph>
Around that core sit thin shells. A shell reads state from a chain,
converts it into plain data, calls the core, and acts on the answer. It
contains almost no decisions, so there is little in it that can be wrong
in an interesting way.
</NoteParagraph>
<NoteFigure caption="Data flows down; only the shells touch the network.">
<CoreShellDiagram />
</NoteFigure>
<NoteSection>The on-chain side</NoteSection>
<NoteParagraph>
The executor contract does one job in one transaction. It takes a flash
loan, liquidates the position, swaps the collateral, repays the loan, and
reverts if the trade would not be profitable. Because the transaction is
atomic, a failed attempt leaves nothing behind except the gas for the
attempt. No own capital is at risk per attempt, which is a property of the
design rather than of careful operation.
</NoteParagraph>
<NoteParagraph>
The same idea applies on chain as off chain: the contract is a small,
closed piece of logic with explicit inputs. The off-chain core decides
whether to try; the contract has the final say.
</NoteParagraph>
<NoteSection>Testing the edge, not the middle</NoteSection>
<NoteParagraph>
Fixed-point arithmetic has cliffs. Past a certain input size a
multiplication no longer fits in the integer type. If the code wraps
around, a hugely healthy position can suddenly look unhealthy, or the
other way round, and nothing crashes to tell you.
</NoteParagraph>
<NoteParagraph>
One spec in the domain crate tests the exact overflow cliff of the health
factor. It pins the behavior one step below the cliff and at the cliff,
and requires the result to saturate instead of wrapping. This kind of case
is where an optimizing refactor or a generated change breaks things
silently, and it is trivial to write because the core is a pure function:
call it with the boundary values and compare the result.
</NoteParagraph>
<NoteSection>Why the split pays off</NoteSection>
<NoteList>
<li>
<strong>The core is testable without a network.</strong> Specs run fast,
with no node, no fork and no flaky timing. That makes spec-first
practical for the part with the most arithmetic.
</li>
<li>
<strong>Mutation testing can prove the specs.</strong> Pure functions
give mutation tools clean targets. If a mutant of the health factor
survives, a spec is missing, and the gate says so. That is a stronger
statement than a coverage percentage.
</li>
<li>
<strong>Shells are proven by end-to-end specs.</strong> They are not
pure, so they are not tested like the core. They are exercised as a
whole against the behavior they are meant to produce, and because they
hold so little logic, those specs stay small.
</li>
</NoteList>
<NoteSection>Where it hurts</NoteSection>
<NoteParagraph>
The discipline has a cost. You have to keep deciding where a line of code
belongs, and the temptation to put a small calculation in the shell
because it is right there is constant. Every such shortcut moves logic
into the part that is hardest to test. The gates described in the note on
mechanical spec-first enforcement help here: a gate that forbids methods,
mutable references and unwrap pushes the code toward the style the core
depends on.
</NoteParagraph>
<NoteParagraph>
The pattern is not specific to this project. Any system where money,
permissions or irreversible actions sit behind a thin layer of I/O gains
from keeping the decision logic pure and the I/O boring.
</NoteParagraph>
</>
);
export const functionalCoreImperativeShell: Note = {
slug: "functional-core-imperative-shell-multi-chain",
title: "Functional core, imperative shell in a multi-chain system",
summary:
"How a pure Rust core, thin shells and an atomic on-chain executor make a 7-chain liquidation system testable.",
publishedOn: "2026-10-02",
readingMinutes: 4,
tags: ["architecture", "rust", "web3"],
Body,
};
+16
View File
@@ -0,0 +1,16 @@
import { sortNotesNewestFirst, validateNoteRegistry } from "@/src/domain/notes";
import { functionalCoreImperativeShell } from "./functional-core-imperative-shell";
import { specFirstGates } from "./spec-first-gates";
import type { Note } from "./types";
const ALL_NOTES: readonly Note[] = [
specFirstGates,
functionalCoreImperativeShell,
];
const validated = validateNoteRegistry(ALL_NOTES);
if (!validated.ok) {
throw new Error(`Invalid notes: ${validated.error.join("; ")}`);
}
export const NOTES: readonly Note[] = sortNotesNewestFirst(validated.value);
@@ -0,0 +1,142 @@
import * as React from "react";
import { GatePipelineDiagram } from "@/src/components/notes/GatePipelineDiagram";
import {
NoteCode,
NoteFigure,
NoteList,
NoteParagraph,
NoteSection,
} from "@/src/components/notes/NoteProse";
import type { Note } from "./types";
const Body: React.FC = () => (
<>
<NoteParagraph>
A workflow that depends on discipline lasts until the first deadline. In
my Rust systems the workflow is enforced by scripts instead: a change that
skips the spec, drops coverage or leaves a weak test does not get past the
gates, no matter who wrote it or how it was generated.
</NoteParagraph>
<NoteSection>The gates</NoteSection>
<NoteParagraph>
There are four, and each one answers a different question.
</NoteParagraph>
<NoteList>
<li>
<strong>TDD gate.</strong> Was the behavior specified as a test before
it was implemented? Skipping the spec fails the gate.
</li>
<li>
<strong>Functional-style gate.</strong> It forbids methods, traits,{" "}
<NoteCode>&amp;mut</NoteCode> and <NoteCode>unwrap</NoteCode> in the
domain code. Pure functions over immutable data are easier to test and
to mutate, so the style is a rule, not a preference. The gate is itself
covered by a self-test script, because a checker that silently stops
checking is worse than none.
</li>
<li>
<strong>Mutation gate.</strong> The tool changes the code in small ways
and the specs must notice. The policy is zero survivors. A surviving
mutant means either a weak assertion or dead code, and both get fixed.
</li>
<li>
<strong>Coverage ratchet.</strong> Coverage may rise but may not fall.
The gate fails when the number drops below the last recorded one.
</li>
</NoteList>
<NoteFigure caption="Four gates in sequence, run at three scopes.">
<GatePipelineDiagram />
</NoteFigure>
<NoteSection>Three speeds</NoteSection>
<NoteParagraph>
Mutation testing is slow, so the same gates run at three scopes. In the
dev loop they look only at changed lines, which keeps feedback fast enough
to use constantly. Before a merge they run on the changed crates. The
merge gate runs everything. Nothing is skipped on the way to main; a
cheaper check just runs earlier and more often.
</NoteParagraph>
<NoteParagraph>
The mutation gate also keeps a proof ledger. When the code bytes and the
toolchain are unchanged since a previous run, the mutants already caught
are carried over instead of being recomputed. That keeps a zero-survivor
policy affordable on a large codebase, and it stays honest because any
change to the code or the toolchain invalidates the entry.
</NoteParagraph>
<NoteSection>Claims drift, measurements do not</NoteSection>
<NoteParagraph>
The most useful lesson came from reading my own documentation. A README
and a Makefile said &quot;100% coverage at all times&quot;, while the
measured badge said 87.5%. Nobody lied; the sentence was true once and
then the code moved. Prose does not fail a build.
</NoteParagraph>
<NoteParagraph>
So I quote the measured number and enforce a ratchet instead of promising
an absolute. A ratchet is a claim the machine checks on every run: the
number may only go up. It is a weaker sentence than &quot;always
100%&quot; and a far more reliable one.
</NoteParagraph>
<NoteSection>A story from this website</NoteSection>
<NoteParagraph>
This site has a small domain core with the same setup: Vitest for the
specs and Stryker for mutation testing. At one point Stryker reported a
mutation score of 14%. That looked like terrible specs, but the specs were
fine. The mutation tool was silently running zero tests per mutant under
Vitest 5, so every mutant looked like it had survived.
</NoteParagraph>
<NoteParagraph>
The cause was a version mismatch between the Stryker Vitest runner and
Vitest itself. Pinning Vitest to 4.1.x fixed it. The first real run then
found 9 genuine gaps in the specs, the kind a green test suite had been
hiding. Rewriting one date check also removed redundant conditions that no
test could ever distinguish from the simpler version.
</NoteParagraph>
<NoteParagraph>
The pin is now written down in the project instructions, with the reason,
so the next dependency bump does not quietly undo it.
</NoteParagraph>
<NoteSection>What I take from it</NoteSection>
<NoteList>
<li>
<strong>Verify that your verification runs.</strong> A gate that
executes zero tests passes or fails for the wrong reasons. Treat &quot;0
tests ran&quot; as a failure, and look at a surprising score before
explaining it away.
</li>
<li>
<strong>Enforce, do not remind.</strong> If a rule matters, a script
should fail when it is broken. Rules that live in a README drift.
</li>
<li>
<strong>Quote measurements.</strong> Write the number the tool printed,
with the command that printed it, and let a ratchet protect it.
</li>
<li>
<strong>Make the cheap check run first.</strong> Three speeds let the
slow, thorough gates exist without slowing the loop that people use
every few minutes.
</li>
</NoteList>
<NoteParagraph>
None of this is specific to Rust. The same shape works for a TypeScript
project: a spec before the code, mutation testing on the changed files,
and a coverage number that is only allowed to go up.
</NoteParagraph>
</>
);
export const specFirstGates: Note = {
slug: "how-i-enforce-spec-first-mechanically",
title: "How I enforce spec-first mechanically",
summary:
"Four gates, three speeds, and one lesson: claims drift, so measure and ratchet instead of promising.",
publishedOn: "2026-10-03",
readingMinutes: 4,
tags: ["testing", "mutation testing", "process"],
Body,
};
+4
View File
@@ -0,0 +1,4 @@
import type * as React from "react";
import type { NoteMeta } from "@/src/domain/notes";
export type Note = NoteMeta & { readonly Body: React.FC };
+193
View File
@@ -0,0 +1,193 @@
import { describe, expect, it } from "vitest";
import {
findNoteBySlug,
sortNotesNewestFirst,
validateNoteMeta,
validateNoteRegistry,
type NoteMeta,
} from "./notes";
const meta = (overrides: Partial<NoteMeta> = {}): NoteMeta => ({
slug: "gate-pipeline",
title: "Gate pipeline",
summary: "A short summary.",
publishedOn: "2026-10-01",
readingMinutes: 5,
tags: ["testing"],
...overrides,
});
describe("sortNotesNewestFirst", () => {
it("orders by publishedOn descending", () => {
const sorted = sortNotesNewestFirst([
meta({ slug: "a", publishedOn: "2026-01-05" }),
meta({ slug: "b", publishedOn: "2026-03-01" }),
meta({ slug: "c", publishedOn: "2025-12-31" }),
]);
expect(sorted.map((n) => n.slug)).toEqual(["b", "a", "c"]);
});
it("breaks ties by slug ascending so the order is deterministic", () => {
const sorted = sortNotesNewestFirst([
meta({ slug: "zeta", publishedOn: "2026-01-05" }),
meta({ slug: "alpha", publishedOn: "2026-01-05" }),
]);
expect(sorted.map((n) => n.slug)).toEqual(["alpha", "zeta"]);
});
it("does not mutate its input", () => {
const input = [
meta({ slug: "a", publishedOn: "2026-01-01" }),
meta({ slug: "b", publishedOn: "2026-02-01" }),
];
sortNotesNewestFirst(input);
expect(input.map((n) => n.slug)).toEqual(["a", "b"]);
});
it("returns an empty list for no notes", () => {
expect(sortNotesNewestFirst([])).toEqual([]);
});
});
describe("findNoteBySlug", () => {
const notes = [meta({ slug: "a" }), meta({ slug: "b" })];
it("returns the matching note", () => {
expect(findNoteBySlug(notes, "b")).toEqual({ ok: true, value: notes[1] });
});
it("returns not-found for an unknown slug", () => {
expect(findNoteBySlug(notes, "c")).toEqual({
ok: false,
error: "not-found",
});
});
});
describe("validateNoteMeta", () => {
it("accepts valid metadata", () => {
expect(validateNoteMeta(meta())).toEqual({ ok: true, value: meta() });
});
it("accepts a summary of exactly 200 characters", () => {
expect(validateNoteMeta(meta({ summary: "x".repeat(200) })).ok).toBe(true);
});
it("rejects a summary of 201 characters", () => {
expect(validateNoteMeta(meta({ summary: "x".repeat(201) }))).toEqual({
ok: false,
error: ["summary-too-long"],
});
});
it.each([
["uppercase", "Gate"],
["spaces", "gate pipeline"],
["underscore", "gate_pipeline"],
["leading hyphen", "-gate"],
["trailing hyphen", "gate-"],
["double hyphen", "gate--pipeline"],
["empty", ""],
])("rejects a slug with %s", (_label, slug) => {
expect(validateNoteMeta(meta({ slug }))).toEqual({
ok: false,
error: ["invalid-slug"],
});
});
it.each(["gate", "gate-pipeline", "a1-b2", "2026-review"])(
"accepts kebab-case slug %s",
(slug) => {
expect(validateNoteMeta(meta({ slug })).ok).toBe(true);
},
);
it.each([
["blank", " "],
["empty", ""],
])("rejects a %s title", (_label, title) => {
expect(validateNoteMeta(meta({ title }))).toEqual({
ok: false,
error: ["empty-title"],
});
});
it.each([
["wrong format", "01.10.2026"],
["impossible day", "2026-02-30"],
["impossible month", "2026-13-01"],
["trailing text", "2026-10-01T00:00"],
["empty", ""],
])("rejects a publishedOn with %s", (_label, publishedOn) => {
expect(validateNoteMeta(meta({ publishedOn }))).toEqual({
ok: false,
error: ["invalid-date"],
});
});
it("accepts a leap day", () => {
expect(validateNoteMeta(meta({ publishedOn: "2028-02-29" })).ok).toBe(true);
});
it("rejects a leap day in a non-leap year", () => {
expect(validateNoteMeta(meta({ publishedOn: "2027-02-29" })).ok).toBe(
false,
);
});
it.each([0, -1, 1.5])("rejects reading minutes %s", (readingMinutes) => {
expect(validateNoteMeta(meta({ readingMinutes }))).toEqual({
ok: false,
error: ["invalid-reading-minutes"],
});
});
it("accepts one reading minute", () => {
expect(validateNoteMeta(meta({ readingMinutes: 1 })).ok).toBe(true);
});
it("reports every problem at once, in a stable order", () => {
expect(
validateNoteMeta(
meta({
slug: "Bad Slug",
title: "",
summary: "x".repeat(201),
publishedOn: "nope",
readingMinutes: 0,
}),
),
).toEqual({
ok: false,
error: [
"invalid-slug",
"empty-title",
"summary-too-long",
"invalid-date",
"invalid-reading-minutes",
],
});
});
});
describe("validateNoteRegistry", () => {
it("accepts a list of valid, unique notes", () => {
const notes = [meta({ slug: "a" }), meta({ slug: "b" })];
expect(validateNoteRegistry(notes)).toEqual({ ok: true, value: notes });
});
it("prefixes each problem with the slug of the offending note", () => {
expect(
validateNoteRegistry([
meta({ slug: "ok" }),
meta({ slug: "bad", title: "" }),
]),
).toEqual({ ok: false, error: ["bad: empty-title"] });
});
it("reports duplicate slugs once per repeated note", () => {
expect(
validateNoteRegistry([meta({ slug: "a" }), meta({ slug: "a" })]),
).toEqual({ ok: false, error: ["a: duplicate-slug"] });
});
});
+83
View File
@@ -0,0 +1,83 @@
import type { Result } from "./availability";
export type NoteMeta = {
readonly slug: string;
readonly title: string;
readonly summary: string;
readonly publishedOn: string;
readonly readingMinutes: number;
readonly tags: readonly string[];
};
export type NoteMetaError =
| "invalid-slug"
| "empty-title"
| "summary-too-long"
| "invalid-date"
| "invalid-reading-minutes";
const KEBAB_CASE = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
const MAX_SUMMARY_LENGTH = 200;
const isRealIsoDate = (value: string): boolean => {
const parsed = new Date(`${value}T00:00:00Z`);
return (
!Number.isNaN(parsed.getTime()) &&
parsed.toISOString().slice(0, 10) === value
);
};
export function sortNotesNewestFirst<T extends NoteMeta>(
notes: readonly T[],
): readonly T[] {
return [...notes].sort((a, b) =>
a.publishedOn === b.publishedOn
? a.slug.localeCompare(b.slug)
: b.publishedOn.localeCompare(a.publishedOn),
);
}
export function findNoteBySlug<T extends { readonly slug: string }>(
notes: readonly T[],
slug: string,
): Result<T, "not-found"> {
const found = notes.find((note) => note.slug === slug);
return found ? { ok: true, value: found } : { ok: false, error: "not-found" };
}
export function validateNoteMeta(
meta: NoteMeta,
): Result<NoteMeta, readonly NoteMetaError[]> {
const errors: readonly NoteMetaError[] = [
...(KEBAB_CASE.test(meta.slug) ? [] : (["invalid-slug"] as const)),
...(meta.title.trim() === "" ? (["empty-title"] as const) : []),
...(meta.summary.length > MAX_SUMMARY_LENGTH
? (["summary-too-long"] as const)
: []),
...(isRealIsoDate(meta.publishedOn) ? [] : (["invalid-date"] as const)),
...(Number.isInteger(meta.readingMinutes) && meta.readingMinutes >= 1
? []
: (["invalid-reading-minutes"] as const)),
];
return errors.length === 0
? { ok: true, value: meta }
: { ok: false, error: errors };
}
export function validateNoteRegistry<T extends NoteMeta>(
notes: readonly T[],
): Result<readonly T[], readonly string[]> {
const metaErrors = notes.flatMap((note) => {
const result = validateNoteMeta(note);
return result.ok ? [] : result.error.map((e) => `${note.slug}: ${e}`);
});
const duplicateErrors = notes
.filter(
(note, index) => notes.findIndex((n) => n.slug === note.slug) !== index,
)
.map((note) => `${note.slug}: duplicate-slug`);
const errors = [...metaErrors, ...duplicateErrors];
return errors.length === 0
? { ok: true, value: notes }
: { ok: false, error: errors };
}