Hedronite · Curriculum · Data · Frontend · Tue 2026-11-10

A ledger audit page that never shows a contradicted release as verified A static export in Bun and TypeScript

Eight deploys in, eight pages out: the ledger's claim is shown, the auditor decides the status, and an audit whose counts do not add up stops the build.

Lesson Class: Curriculum · F-Data · rung A
Author: Evan Eugene Pincham
Tested with: Bun 1.3 (lab image oven/bun:1.3-debian)
Lab: lab/ · sh check.sh (8 checks) · no network, no browser
Source: Designing Data-Intensive Applications ch.11 · CloudTrail log file integrity validation · CloudWatch Logs filter pattern syntax · DOP-C02 v1.6 Task 4.2 · Bun File I/O · Lustre v5.7.1 · WAI-ARIA APG Alert

Lab download. Starter is the tree you edit. Worked answer is a separate file; open it after the starter passes. Bun 1.3 or newer. package.json has no dependencies to install. All lab requirements.

The Claim Is Shown, Not Believed
The ledger's own words appear in a column; the status comes only from the audit.
Audit Both Directions
An edited line is contradicted by number; a deleted line is an omission named with its release.
Counts Must Add Up
An audit whose summary disagrees with its own violations or with the ledger is a build error, not a page.
A well-formed ledger can still be false; the page waits for the audit before it says verified.

F-Data · A · arc: self-healing-edge · langs: ts, gleam · DOP-4.2

Concept

The situation. A release tool keeps a log of what it did. Call that log the ledger: an append-only file of JSON objects, one per line (this format is called JSONL, "JSON Lines"). A release is one version of a service, such as v1 or v2. After a new release is switched on, the tool watches it for a fixed soak window, here 30 000 milliseconds. If the release crashes more than a set restart budget (here 2) inside the window, the tool performs a rollback: it switches back to the last good release and writes a rolled_back line. If the window ends quietly, the tool writes a confirmed line.

A person reading a status page wants one question answered: can I believe what the ledger says happened?

The trap. The easy page reads the ledger and shows "verified" for every release that has a confirmed or rolled_back line. That page repeats the ledger's own words. Anyone who edits the ledger edits the page.

Plan, ledger and auditor. This lesson uses three inputs.

The derivation rule is short. For each promoted release, if the plan shows the third exit inside the soak window, the ledger must contain exactly one rolled_back at that exit's timestamp (ts, in milliseconds). Otherwise the first plan event at or after promotion + 30 000 closes the window, and the ledger must contain exactly one confirmed. The rollback target must be the last release whose derived outcome was confirmed, or the baseline.

Why re-derive. Martin Kleppmann describes an event log as the source of truth from which current state is computed. The computation "should be deterministic, so that you can run it again and derive the same application state from the event log." If the computation is deterministic, replaying the log is a check: derive the state again and compare. The same chapter explains why accountants keep an append-only ledger. A mistake is corrected by adding a compensating entry, and the incorrect entry stays "because it might be important for auditing reasons." An audit is therefore a legitimate thing to do to a ledger. Editing an entry silently is the thing an audit exists to catch.

Two directions. Comparing both ways matters, because forgery takes two shapes.

  1. A claim without support: a line was added or edited. Example: a confirmed stamped at 12 000 when the plan's window only closes at 31 000.
  2. A derived fact without a claim: a line was deleted. The plan says a rollback must have happened, and the ledger has none.

A deleted line leaves no line number to point at, so the auditor prints line=none and the page shows "omission (no line)". Tools that only look at lines that exist cannot see this case. The Amazon CloudWatch Logs documentation says of its filter patterns that they "only return the log events that contain the terms you define", which is a good description of a tool that is blind to what is missing.

A schema check is not an audit. A schema says what shape a line must have. The Rust auditor's schema rule checks that ts is an unsigned integer and that event and version are strings, using Value::as_u64 and Value::as_str from the serde_json library, which return None when the value is another type. A forged ledger can pass that check on every line. The lab is built around that failure: the ledger is well formed and false.

The three statuses. The page assigns each release exactly one status, and the status comes from the auditor's output, never from the ledger's claim. The claim is shown beside it for the reader.

StatusWhen
contradictedat least one violation names this release
unverifiedno violation names it, but a damaged line with no readable version (version=-) could have concerned it, or the ledger makes no outcome claim yet
verifiedotherwise (the baseline, the plan's starting release, is verified this way when nothing contradicts it)

Two build errors. The page is a static export: a build step writes plain HTML files, and there is no server to run. That means mistakes have to be caught at build time, and the build is where we make them impossible to ship.

What this does not do. This is not tamper-proof. It detects a ledger that is inconsistent with the plan. A person who edits the plan, the ledger and the audit output consistently is not caught. The page also trusts that the audit file really came from running the auditor. The check is about inconsistency, and it is only as strong as the independence of the plan.

Worked example

Everything starts from three small deploys. Each has a plan and an honest ledger. The honest ledgers are exactly what the release tool printed for the same plans.

FixturePlanHonest ledger
flapv2 promoted at 1 000, exits at 3, 6 and 9 secondstwo restarting lines, then rolled_back to v1 at 9 000
stablev2 promoted, one exit at 3 000, a tick at 31 000one restarting line, then confirmed at 31 000
chainv2 is rolled back; v3 is promoted with a stale previous of v2; v3 is rolled backrollbacks of v2 and then v3, both to v1

The promoted line carries a previous field the auditor never reads. In chain it is deliberately stale (v3 says previous=v2, but v2 was rolled back). The auditor derives the rollback target from outcomes, so it must still say v1.

Four forgeries are built from these, plus damaged and unfinished cases:

What the auditor says

Each fixture has a recorded auditor output. This is the content of the early and deleted audit files, exactly as the checker writes them:

VIOLATION line=2 rule=confirmed_before_window version=v2 confirmed at 12000 but the window closes at 31000
ledger: lines=2 violations=1
VIOLATION line=none rule=missing_rollback version=v2 the plan rolls it back at 9000
ledger: lines=2 violations=1

The format is VIOLATION line=<n|none> rule=<rule> version=<v> <detail>, followed by exactly one summary line ledger: lines=<n> violations=<k>. The none marks an omission. The damaged fixture shows the schema case: a line that cannot be parsed is counted, named, and the audit continues. Its first violation reads VIOLATION line=2 rule=schema version=- ..., with version=- because nothing in the cut line could be trusted to name a release.

The reader module

The module has one job: turn (plan, ledger, audit text) into a list of releases with statuses and a list of violations. It does three things in order.

1. List the releases from the plan. The baseline is the serving event's version, then each promotion in order. The ledger is not consulted for this. A release cannot appear on the page because the ledger mentions it.

2. Validate the audit before believing it. These are the lines that turn a miscount into a build error:

  if (!summary) throw new Error("audit has no summary line");
  const [lines, count] = [Number(summary[1]), Number(summary[2])];
  // Counts that do not add up are a build error, not a page.
  if (count !== violations.length) throw new Error(`summary says ${count} violations, audit lists ${violations.length}`);
  if (lines !== ledgerLines.length) throw new Error(`summary says ${lines} ledger lines, ledger has ${ledgerLines.length}`);
  for (const v of violations) {
    if (v.line !== "none" && (v.line < 1 || v.line > lines)) throw new Error(`violation names line ${v.line}, ledger has ${lines}`);
    if (v.version !== "-" && !order.includes(v.version)) throw new Error(`violation names ${v.version}, which the plan never served`);
  }

Note what these catch. A summary that undercounts (short), a summary placed before a violation (hidden, where the single summary line says zero and a violation follows), an audit run against the wrong ledger (wrongfile), and a violation about a release nobody served (stranger).

3. Take the status from the violations. The ledger's claim is read only to display it:

  const unattributed = violations.some((v) => v.version === "-");
  const releases = order.map((version): Release => {
    const mine = violations.filter((v) => v.version === version);
    const claim = claims.get(version) ?? null;
    const isBase = version === baseline;
    if (mine.length > 0) return { version, baseline: isBase, claim, status: "contradicted", why: `${mine.length} violation(s) name this release` };
    if (unattributed) return { version, baseline: isBase, claim, status: "unverified", why: "a damaged ledger line could not be attributed to a release" };
    if (!isBase && claim === null) return { version, baseline: isBase, claim, status: "unverified", why: "the ledger makes no outcome claim yet" };
    return { version, baseline: isBase, claim, status: "verified", why: isBase ? "the plan's starting release" : "the audit re-derived this claim from the plan" };
  });

The order of the tests is the policy. Contradiction wins over everything. A damaged line that cannot be attributed makes every release in that deploy at most unverified, because it could have concerned any of them. Only then do we ask whether there is a claim at all.

The static export

build.ts writes one HTML page per deploy using Bun. Bun's file API reads a file with Bun.file(path).text() and writes one with Bun.write(dest, data), so the build needs no dependencies. Every value that came from a file goes through esc, which escapes &, <, > and ", because a ledger is untrusted text and could contain markup. The banner uses role="alert" when there are violations, following the WAI-ARIA Authoring Practices "Alert Pattern", which describes an alert as "a brief, important message", and role="status" when the audit is clean. One diagnostic worth knowing: the same pattern warns that "screen readers do not inform users of alerts that are present on the page before page load completes." A static page is complete at load, so the role here labels the banner rather than announcing it. The banner is the first thing after the heading, which is what actually puts it in front of a screen-reader user.

build.ts, in full:

// Static export: one ledger-audit page per deploy. No server, no client JS.
// Usage: bun build.ts <fixtures-dir> <out-dir>. A deploy whose audit does not add up stops the build.
import { readdirSync, mkdirSync } from "node:fs";
import { readDeploy, type Deploy } from "./audit.ts";

const [inDir = "fixtures", outDir = "dist"] = process.argv.slice(2);
const esc = (s: string) =>
  s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");

function page(name: string, d: Deploy): string {
  const rows = d.releases.map((r) =>
    `<tr class="${r.status}" data-version="${esc(r.version)}" data-status="${r.status}"><td>${esc(r.version)}${r.baseline ? " (baseline)" : ""}</td>` +
    `<td>${esc(r.claim ?? "no outcome claim")}</td><td>${r.status}: ${esc(r.why)}</td></tr>`).join("\n");
  const items = d.violations.map((v) =>
    `<li data-line="${v.line}" data-rule="${v.rule}" data-violation-version="${esc(v.version)}">` +
    `${v.line === "none" ? "omission (no line)" : `line ${v.line}`} · ${v.rule} · ${esc(v.version)} · ${esc(v.detail)}</li>`).join("\n");
  const clean = d.violations.length === 0;
  return `<!doctype html>
<html lang="en"><head><meta charset="utf-8"><title>ledger audit: ${esc(name)}</title>
<link rel="stylesheet" href="audit.css"></head>
<body><main data-lines="${d.lines}" data-violations="${d.violations.length}">
<h1>Ledger audit</h1>
<p class="banner ${clean ? "ok" : "error"}" role="${clean ? "status" : "alert"}">${clean
    ? `The audit re-derived all ${d.lines} ledger lines from the plan and found no violations.`
    : `${d.violations.length} violation(s) in ${d.lines} ledger lines; the ledger's own words are not evidence.`}</p>
<table><thead><tr><th>Release</th><th>Ledger claims</th><th>Audit</th></tr></thead><tbody>
${rows}
</tbody></table>
<h2>Violations</h2>
${clean ? `<p data-field="violations">none</p>` : `<ol data-field="violations">\n${items}\n</ol>`}
</main></body></html>
`;
}

// The page must never say verified next to a release a violation names. Check the HTML we wrote.
function selfCheck(html: string, d: Deploy) {
  for (const v of d.violations) {
    if (html.includes(`data-version="${esc(v.version)}" data-status="verified"`))
      throw new Error(`page shows verified for ${v.version}, which a violation names`);
  }
}

mkdirSync(outDir, { recursive: true });
let bad = 0;
for (const file of readdirSync(inDir).filter((f) => f.endsWith(".plan")).sort()) {
  const name = file.replace(/\.plan$/, "");
  const read = (ext: string) => Bun.file(`${inDir}/${name}.${ext}`).text();
  try {
    const d = readDeploy(await read("plan"), await read("ledger"), await read("audit"));
    const html = page(name, d);
    selfCheck(html, d);
    await Bun.write(`${outDir}/${name}.html`, html);
    console.log(`${name} -> ${outDir}/${name}.html ` + d.releases.map((r) => `${r.version}=${r.status}`).join(" "));
  } catch (e) {
    console.error(`build error: ${name}: ${(e as Error).message}`);
    bad++;
  }
}
await Bun.write(`${outDir}/audit.css`, await Bun.file("audit.css").text());
if (bad > 0) {
  console.error(`build failed: ${bad} deploy(s) whose audit does not add up`);
  process.exit(1);
}

selfCheck is a second line of defense. It inspects the HTML that was just built and refuses to write it if a violation-named release is marked verified. It is not the main control. If it were, a bug that marks a release unverified when it should be contradicted would pass it. The lab shows exactly that case.

package.json and audit.css, both unchanged between starter and reference:

{
  "name": "ledger-audit-page",
  "private": true,
  "type": "module",
  "scripts": { "build": "bun build.ts fixtures dist" }
}
body { font: 16px/1.5 system-ui, sans-serif; max-width: 46rem; margin: 2rem auto; padding: 0 1rem; }
table { border-collapse: collapse; width: 100%; }
td, th { border-bottom: 1px solid #ccc; padding: .4rem; text-align: left; }
.banner { padding: .6rem; border-left: 4px solid; }
.banner.ok { border-color: #2e7d32; } .banner.error { border-color: #c62828; }
tr.verified td:last-child::before { content: "✓ "; }
tr.contradicted td:last-child::before { content: "✗ "; }
tr.unverified td:last-child::before { content: "? "; }

The CSS prefixes each status with a mark (✓, ✗, ?), so the status does not rely on colour alone.

The checker

The lab ships a POSIX shell script, check.sh. It builds every fixture itself, runs the build, and prints one RESULT line per check. The eight checks:

CheckWhat it requires
static_buildthe export builds and writes 8 pages
honest_ledgers_verifiedflap, stable, chain: every release verified, claims shown, "none" under violations
edited_claim_contradictedearly and badtarget: the edited release is contradicted, the line is listed by number
omission_nameddeleted: contradicted, listed as "omission (no line)" with its release
damaged_ledger_unverifieddamaged: all four violations listed, v2 contradicted, v1 unverified
no_claim_unverifiedsoaking: the release with no outcome claim is unverified, not verified
never_verified_with_violationacross every built page, no violation-named release is verified
miscount_fails_buildthe four hand-edited audits (short, hidden, wrongfile, stranger) fail the build and write no page

The continuous-integration gate ran the checker twice, once on the reference solution and once on the starter, in the oven/bun:1.3-debian container image with networking off. This is the reference section, copied from the gate log:

## reference (rc=0, PASS)
badtarget -> dist/badtarget.html v1=verified v2=verified v3=contradicted
chain -> dist/chain.html v1=verified v2=verified v3=verified
damaged -> dist/damaged.html v1=unverified v2=contradicted
deleted -> dist/deleted.html v1=verified v2=contradicted
early -> dist/early.html v1=verified v2=contradicted
flap -> dist/flap.html v1=verified v2=verified
soaking -> dist/soaking.html v1=verified v2=unverified
stable -> dist/stable.html v1=verified v2=verified
built 8 pages
RESULT static_build PASS
honest: flap v2=verified stable v2=verified chain v2=verified v3=verified
RESULT honest_ledgers_verified PASS
early: v2=contradicted; badtarget: v2=verified v3=contradicted
RESULT edited_claim_contradicted PASS
deleted: v2=contradicted
RESULT omission_named PASS
damaged: v1=unverified v2=contradicted listed=4
RESULT damaged_ledger_unverified PASS
soaking: v1=verified v2=unverified
RESULT no_claim_unverified PASS
violation-named releases checked: 5
RESULT never_verified_with_violation PASS
RESULT miscount_fails_build PASS
$ bun build.ts fixtures dist
build error: hidden: summary says 0 violations, audit lists 1
build error: short: summary says 2 violations, audit lists 1
build error: stranger: violation names v9, which the plan never served
build error: wrongfile: summary says 6 ledger lines, ledger has 3
build failed: 4 deploy(s) whose audit does not add up

Read the badtarget line closely: v2=verified v3=contradicted. The forgery edited v3's rollback, so only v3 is contradicted. v2 is still verified, because the audit found nothing wrong with it.

Lab

Goal. Starting from a page generator that trusts the ledger, make it take every status from the audit and make a miscounted audit a build error. Success is sh check.sh printing eight PASS lines.

Setup. You need Bun and a shell. Create an empty folder and put the lab's starter files in it: package.json, audit.css, build.ts (the three files shown above, identical to the starter's), the starter audit.ts, and check.sh. No packages are installed and no network is needed.

Step 1: see it fail. Run:

sh check.sh

The starter's audit.ts marks a release verified whenever the ledger claims an outcome. This is the starter section of the gate log. The RESULT lines in your run should match it; the interleaving of build messages can differ, because the build writes errors to stderr and page lines to stdout:

## starter (rc=1, FAIL)
chain -> dist/chain.html v1=verified v2=verified v3=verified
damaged -> dist/damaged.html v1=verified v2=unverified
deleted -> dist/deleted.html v1=verified v2=unverified
flap -> dist/flap.html v1=verified v2=verified
soaking -> dist/soaking.html v1=verified v2=unverified
stable -> dist/stable.html v1=verified v2=verified
RESULT static_build FAIL
honest: flap v2=verified stable v2=verified chain v2=verified v3=verified
RESULT honest_ledgers_verified PASS
early: v2=; badtarget: v2= v3=
RESULT edited_claim_contradicted FAIL
deleted: v2=unverified
RESULT omission_named FAIL
damaged: v1=verified v2=unverified listed=4
RESULT damaged_ledger_unverified FAIL
soaking: v1=verified v2=unverified
RESULT no_claim_unverified PASS
violation-named releases checked: 3
RESULT never_verified_with_violation FAIL
stranger -> dist-rejected/stranger.html v1=verified v2=verified
wrongfile -> dist-rejected/wrongfile.html v1=verified v2=verified
rejected: build failed but wrote: dist-rejected/stranger.html dist-rejected/wrongfile.html 
RESULT miscount_fails_build FAIL
$ bun build.ts fixtures dist
build error: badtarget: page shows verified for v3, which a violation names
build error: early: page shows verified for v2, which a violation names
build failed: 2 deploy(s) whose audit does not add up
error: script "build" exited with code 1
build error: hidden: page shows verified for v2, which a violation names
build error: short: page shows verified for v2, which a violation names
build failed: 2 deploy(s) whose audit does not add up

Notice three things before you edit anything.

Step 2: validate the audit. The starter's loop over the audit text contains the two places to change:

  let lines = ledgerLines.length;
  for (const l of nonEmpty(audit)) {
    const v = l.match(VIOLATION);
    const s = l.match(SUMMARY);
    if (v) violations.push({ line: v[1] === "none" ? "none" : Number(v[1]), rule: v[2], version: v[3], detail: v[4] });
    if (s) lines = Number(s[1]); // TODO: check the summary against the audit and the ledger
  }

Replace this with a loop that accepts only violation lines and exactly one summary line, and throws on anything else. Then throw if there is no summary, if the summary's violation count differs from the number of violations listed, if its line count differs from the number of non-empty lines in the ledger, if a violation names a line outside 1..lines, or if a violation names a version (other than -) that the plan never served. Any message will do for check 8, which tests that the build fails and writes no page; the messages in the gate log above are the reference's, and matching them makes your output easy to compare.

Step 3: take the status from the audit. The starter's second TODO:

    // TODO: a violation that names this release must win over the ledger's claim.
    if (isBase || claim !== null) return { version, baseline: isBase, claim, status: "verified", why: "the ledger says so" };

Write the four-way decision from the Concept section, in that order: a violation names it → contradicted; any version=- violation exists → unverified; a non-baseline release with no claim → unverified; otherwise verified. Keep the claim in the returned object: the page still displays it, it just no longer decides anything.

Step 4: run it again. sh check.sh should print the same eight passing RESULT lines as the reference section above and exit 0.

Step 5 (extension, no solution given). Two exercises:

  1. Add a ninth fixture to check.sh whose ledger claims confirmed for a release the plan never promoted. The auditor reports confirmed_unsupported for that case. Write the .audit file the auditor would print, and decide what status the page should show for the release.
  2. Hand-edit one .audit file so that it says violations=0 for a forged ledger, and run the build. Does the page catch it? It should not, and that is the point of the honesty line below: the page can only compare the audit with the ledger on counts and line numbers, not re-derive anything.

Reference solution. Try the steps first. This is the finished audit.ts:

// Read one deploy: the plan, the ledger, and the auditor's text output.
// The page never trusts the ledger's own words; a release's status comes from the audit.
export type Status = "verified" | "unverified" | "contradicted";
export interface Violation { line: number | "none"; rule: string; version: string; detail: string }
export interface Release { version: string; baseline: boolean; claim: string | null; status: Status; why: string }
export interface Deploy { releases: Release[]; violations: Violation[]; lines: number }

const VIOLATION = /^VIOLATION line=(\d+|none) rule=([a-z_]+) version=(\S+) ?(.*)$/;
const SUMMARY = /^ledger: lines=(\d+) violations=(\d+)$/;
const nonEmpty = (s: string) => s.split("\n").filter((l) => l.trim() !== "");

export function readDeploy(plan: string, ledger: string, audit: string): Deploy {
  // Releases come from the plan: the serving baseline, then each promotion in order.
  const order: string[] = [];
  let baseline = "";
  for (const l of nonEmpty(plan)) {
    const e = JSON.parse(l);
    if (e.event === "serving") baseline = e.version;
    if ((e.event === "serving" || e.event === "promoted") && !order.includes(e.version)) order.push(e.version);
  }
  if (!baseline) throw new Error("plan has no serving baseline");

  // The ledger's claims, read leniently: a damaged line is the audit's to name, not ours.
  const ledgerLines = nonEmpty(ledger);
  const claims = new Map<string, string>();
  for (const l of ledgerLines) {
    let e: any;
    try { e = JSON.parse(l); } catch { continue; }
    if (e.event === "confirmed") claims.set(e.version, "confirmed");
    if (e.event === "rolled_back") claims.set(e.version, `rolled_back to ${e.to}`);
  }

  // The audit's text output: every VIOLATION line, then exactly one summary line.
  const violations: Violation[] = [];
  let summary: RegExpMatchArray | null = null;
  for (const l of nonEmpty(audit)) {
    const v = l.match(VIOLATION);
    const s = l.match(SUMMARY);
    if (v) violations.push({ line: v[1] === "none" ? "none" : Number(v[1]), rule: v[2], version: v[3], detail: v[4] });
    else if (s && !summary) summary = s;
    else throw new Error(`audit line not understood: ${l}`);
  }
  if (!summary) throw new Error("audit has no summary line");
  const [lines, count] = [Number(summary[1]), Number(summary[2])];
  // Counts that do not add up are a build error, not a page.
  if (count !== violations.length) throw new Error(`summary says ${count} violations, audit lists ${violations.length}`);
  if (lines !== ledgerLines.length) throw new Error(`summary says ${lines} ledger lines, ledger has ${ledgerLines.length}`);
  for (const v of violations) {
    if (v.line !== "none" && (v.line < 1 || v.line > lines)) throw new Error(`violation names line ${v.line}, ledger has ${lines}`);
    if (v.version !== "-" && !order.includes(v.version)) throw new Error(`violation names ${v.version}, which the plan never served`);
  }

  // A damaged line with no readable version could have concerned any release.
  const unattributed = violations.some((v) => v.version === "-");
  const releases = order.map((version): Release => {
    const mine = violations.filter((v) => v.version === version);
    const claim = claims.get(version) ?? null;
    const isBase = version === baseline;
    if (mine.length > 0) return { version, baseline: isBase, claim, status: "contradicted", why: `${mine.length} violation(s) name this release` };
    if (unattributed) return { version, baseline: isBase, claim, status: "unverified", why: "a damaged ledger line could not be attributed to a release" };
    if (!isBase && claim === null) return { version, baseline: isBase, claim, status: "unverified", why: "the ledger makes no outcome claim yet" };
    return { version, baseline: isBase, claim, status: "verified", why: isBase ? "the plan's starting release" : "the audit re-derived this claim from the plan" };
  });
  return { releases, violations, lines };
}

Lab verification: the reference and the starter were both run by the lab's gate in the oven/bun:1.3-debian image with networking off, on 2026-11-10. The reference passed all eight checks; the starter failed six of them, as designed.

Limits of this lab, stated plainly. The audit files are recorded outputs. The page checks that an audit is internally consistent and consistent with the ledger's line count. It cannot check that the audit was honestly produced. That is a property of how the auditor is run and who can change its inputs.

Other paths

Gleam with Lustre (sketch, not run). Gleam is a statically typed language that compiles to JavaScript or to the Erlang VM. Lustre is a Gleam library for HTML; its docs (v5.7.1) say it can be used "as a HTML templating library, without using its runtime features", which is exactly a static export. attribute.data(key, value) adds a data-* attribute, element.text turns a string into a text node, and element.to_string renders the tree. The function names below were checked against the v5.7.1 module docs; the sketch itself has not been compiled or run.

// SKETCH, NOT RUN. Names checked against the Lustre v5.7.1 docs
// for lustre/attribute, lustre/element and lustre/element/html.
import gleam/list
import lustre/attribute
import lustre/element
import lustre/element/html

pub type Status {
  Verified
  Unverified
  Contradicted
}

pub type Release {
  Release(version: String, claim: String, status: Status)
}

fn status_name(s: Status) -> String {
  case s {
    Verified -> "verified"
    Unverified -> "unverified"
    Contradicted -> "contradicted"
  }
}

fn row(r: Release) -> element.Element(msg) {
  html.tr(
    [
      attribute.data("version", r.version),
      attribute.data("status", status_name(r.status)),
    ],
    [
      html.td([], [element.text(r.version)]),
      html.td([], [element.text(r.claim)]),
      html.td([], [element.text(status_name(r.status))]),
    ],
  )
}

pub fn table(releases: List(Release)) -> String {
  html.table([], [html.tbody([], list.map(releases, row))])
  |> element.to_string
}

What the type buys you: Status has exactly three constructors, so a misspelt status is a compile error, and a case over it must handle all three. What it does not buy you: the type says nothing about who decides the status. The audit-derived rule (violations first, then unattributed damage, then missing claims) still has to live in one function that is the only place a Status is created. The build-time rule ("no verified next to a violation-named release") and the count checks would be ported the same way as in TypeScript. On escaping: the Lustre docs single out unsafe_raw_html as the function that "will not escape the HTML string provided", so ledger text belongs in element.text, never in that function.

CloudTrail log file integrity validation (prose contrast). AWS CloudTrail records API activity as log files. Its log file integrity validation feature, in the AWS CloudTrail User Guide's "Validating CloudTrail log file integrity", exists, in the guide's words, "To determine whether a log file was modified, deleted, or unchanged after CloudTrail delivered it". It does this with hashes and signatures: SHA-256 for hashing and SHA-256 with RSA for digital signing, with hourly digest files that are each signed and contain the signature of the previous digest file. That chains the digests together. The contrast with this page is the question each asks. CloudTrail validation detects modified or deleted log files: whether the bytes are what was delivered. This page detects inconsistent logs: whether the content agrees with an independent record of what must have happened. They are complementary. A tamper-evident ledger can still be false when it is written, and a consistent ledger can still be silently replaced if nothing signs it.

Citations