Skip to content

Commit 5e520d5

Browse files
noonioclaude
andcommitted
Migrate the specification prose from LaTeX to Typst
Replace the LaTeX/TikZ/SVG spec sources with a literate-Typst tree rendered by Typst: - Prose, notation macros, and native cetz/fletcher diagrams replace the .tex sources, TikZ/SVG figures, and the stripped JuliaMono fonts. - build.sh renders the .lagda.typ tree with Typst (+ the notation-tooltip postprocess); the Shakefile/LaTeX toolchain is removed. - The @Preview diagram packages (cetz, fletcher, oxifmt) are supplied at pinned versions from nixpkgs via `typst.withPackages` (TYPST_PACKAGE_CACHE_PATH), keeping the build hermetic without vendoring them into the repo. - Dev shell and `just spec` use the same wrapped typst. The literate sources carry only prose here; the Agda code blocks, the standalone reference modules, and the Agda typecheck/lint stage are added in the following change, so the formalisation appendix is empty for now. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 6f9eb07 commit 5e520d5

65 files changed

Lines changed: 4657 additions & 3953 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

flake.lock

Lines changed: 17 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

flake.nix

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,11 @@
1515
mithril.url = "github:input-output-hk/mithril/2617.0";
1616
nixpkgs.follows = "haskellNix/nixpkgs";
1717
nixpkgs-2411.url = "github:NixOS/nixpkgs/nixos-24.11";
18+
# For typst >= 0.14.1 only: the main pin's typst 0.14.0 emits the PDF
19+
# named-destination name tree unsorted (typst#7248), which kills internal
20+
# section links in spec-compliant viewers. Drop when the main nixpkgs pin
21+
# ships typst >= 0.14.1.
22+
nixpkgs-2511.url = "github:NixOS/nixpkgs/nixos-25.11";
1823
nix-npm-buildpackage.url = "github:serokell/nix-npm-buildpackage";
1924
process-compose-flake.url = "github:Platonic-Systems/process-compose-flake";
2025
rust-accumulator.url = "github:cardano-scaling/rust-accumulator";

justfile

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,3 +117,10 @@ lint PKG="all":
117117
-Wmissing-deriving-strategies \
118118
-Wredundant-constraints \
119119
-Wunused-packages"
120+
121+
# Needs agda + typst (both in the nix dev shell); mirrors `nix build .#spec` but
122+
# runs in place for fast iteration.
123+
#
124+
# Agda typechecks the literate-Typst sources, then Typst renders spec/_build/hydra-spec.pdf.
125+
spec:
126+
spec/build.sh

nix/configuration.nix

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,10 @@
1010
compiler = "ghc967";
1111
inputMap = { "https://intersectmbo.github.io/cardano-haskell-packages" = inputs.CHaP; };
1212
pkgs-2411 = import inputs.nixpkgs-2411 { inherit system; };
13+
pkgs-2511 = import inputs.nixpkgs-2511 { inherit system; };
1314
in
1415
{
15-
_module.args = { inherit compiler inputMap pkgs-2411; };
16+
_module.args = { inherit compiler inputMap pkgs-2411 pkgs-2511; };
1617
legacyPackages = pkgs // hsPkgs;
1718
};
1819
}

nix/hydra/shell.nix

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,20 @@
44

55
{ self, ... }: {
66

7-
perSystem = { pkgs, hsPkgs, compiler, self', inputs', ... }:
7+
perSystem = { pkgs, hsPkgs, compiler, self', inputs', pkgs-2511, ... }:
88
let
99
# Clean nixpkgs (no haskell.nix / nix-npm-buildpackage overlays). The
1010
# overlaid `pkgs` routes node packages through nix-npm-buildpackage,
1111
# which fails to build playwright-test, so take it from upstream instead.
1212
cleanPkgs = inputs'.nixpkgs.legacyPackages;
1313

1414
buildInputs = [
15+
# For working on the formal specification (typst render + the
16+
# annotate-notation.py tooltip postprocess, see spec/build.sh). The
17+
# wrapped typst carries the pinned @preview diagram packages via
18+
# TYPST_PACKAGE_CACHE_PATH, same as the nix build (see nix/hydra/spec.nix).
19+
self'.packages.spec-typst
20+
(pkgs-2511.python3.withPackages (ps: [ ps.pymupdf ]))
1521
# To compile hydra scripts
1622
pkgs.aiken
1723
pkgs.cabal-fmt
@@ -88,6 +94,10 @@
8894

8995
CREATE_MISSING_GOLDEN = 1;
9096

97+
# Code font for the spec PDF; spec/build.sh passes it to typst
98+
# (`just spec`), mirroring the hermetic `nix build .#spec`.
99+
JULIAMONO_FONT_DIR = "${pkgs.julia-mono}/share/fonts/truetype";
100+
91101
# Force a UTF-8 locale because many Haskell programs and tests
92102
# assume this.
93103
LANG = "en_US.UTF-8";

nix/hydra/spec.nix

Lines changed: 71 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,36 +1,86 @@
1-
{ self, inputs, ... }: {
1+
{ self, ... }: {
22

3-
perSystem = { pkgs, pkgs-2411, ... }:
3+
perSystem = { config, pkgs, pkgs-2511, ... }:
44
let
5-
agdaPackages = pkgs-2411.callPackage "${self}/spec/pkgs/initial-packages.nix" {
6-
inherit (pkgs-2411.haskellPackages) Agda;
7-
nixpkgs = inputs.nixpkgs-2411;
5+
# Typst with the spec's diagram packages (@preview/cetz, fletcher, oxifmt)
6+
# pinned from nixpkgs and supplied via the wrapper's
7+
# TYPST_PACKAGE_CACHE_PATH, so the build stays hermetic without vendoring
8+
# them into the repo. typst >= 0.14.1 (0.14.0 emits the PDF
9+
# named-destination name tree unsorted, typst#7248, killing internal
10+
# section links in viewers that binary-search it per spec: pdf.js, PDFium,
11+
# macOS Preview).
12+
spec-typst = pkgs-2511.typst.withPackages (p: [
13+
p.cetz_0_3_4
14+
p.fletcher_0_5_8
15+
p.oxifmt_0_2_1
16+
]);
17+
18+
# The Typst render, WITHOUT the notation-tooltip postprocess
19+
# (ANNOTATE_NOTATION=skip, see build.sh stage 3). Internal: consume
20+
# packages.spec, which adds the tooltips.
21+
#
22+
# The postprocess runs as the separate seconds-long derivation below so
23+
# this minutes-long, disk-heavy build shares no build window with the
24+
# python closure: a busy builder's mid-build auto-GC (observed on the
25+
# aarch64-darwin CI builders, where no sandbox bind-mount keeps a
26+
# collected path alive for a running build) once collected a late-used
27+
# python package out from under the final build step. Inputs are
28+
# re-validated when each derivation starts, so splitting shrinks the
29+
# exposure of the python environment from the whole render to seconds.
30+
spec-rendered = pkgs.stdenv.mkDerivation {
31+
pname = "hydra-spec-unannotated.pdf";
32+
version = "0.0.1";
33+
nativeBuildInputs = [
34+
config.packages.spec-typst
35+
];
36+
meta = { };
37+
src = "${self}/spec";
38+
# build.sh renders the literate-Typst sources with Typst (no
39+
# LaTeX/Inkscape toolchain needed). --ignore-system-fonts keeps Typst
40+
# reproducible: only the fonts bundled with Typst plus JuliaMono from
41+
# nixpkgs (code blocks, wired through JULIAMONO_FONT_DIR, see build.sh)
42+
# are used.
43+
JULIAMONO_FONT_DIR = "${pkgs.julia-mono}/share/fonts/truetype";
44+
ANNOTATE_NOTATION = "skip";
45+
buildPhase = ''
46+
export HOME=$TMPDIR
47+
bash build.sh
48+
'';
49+
installPhase = ''
50+
mkdir $out
51+
cp _build/hydra-spec.pdf $out/hydra-spec.pdf
52+
'';
853
};
9-
agdaLibraries = with agdaPackages; [
10-
abstract-set-theory
11-
formal-ledger
12-
standard-library
13-
standard-library-classes
14-
standard-library-meta
15-
];
1654
in
1755
{
18-
packages.spec = agdaPackages.mkDerivation {
56+
# Typst with the spec's diagram packages, reused by the spec build and
57+
# exposed so the dev shell can offer the same `typst` for working on the spec.
58+
packages.spec-typst = spec-typst;
59+
60+
# The publishable spec PDF: the render above plus the notation hover
61+
# tooltips (build.sh stage 3, split out - see the spec-rendered comment).
62+
packages.spec = pkgs.stdenv.mkDerivation {
1963
pname = "hydra-spec.pdf";
2064
version = "0.0.1";
21-
nativeBuildInputs = with pkgs; [
22-
(agdaPackages.withPackages agdaLibraries)
23-
(haskellPackages.ghcWithPackages (p: [ p.shake ]))
24-
inkscape
25-
texlive.combined.scheme-full
65+
nativeBuildInputs = [
66+
# for annotate-notation.py (stamps the tooltips, needs PyMuPDF)
67+
(pkgs-2511.python3.withPackages (ps: [ ps.pymupdf ]))
2668
];
2769
meta = { };
28-
src = "${self}/spec";
29-
buildPhase = "shake";
70+
dontUnpack = true;
71+
buildPhase = ''
72+
python3 ${self}/spec/annotate-notation.py \
73+
${spec-rendered}/hydra-spec.pdf hydra-spec.pdf
74+
'';
3075
installPhase = ''
3176
mkdir $out
32-
cp _build/hydra-spec.pdf $out/hydra-spec.pdf
77+
cp hydra-spec.pdf $out/hydra-spec.pdf
3378
'';
3479
};
80+
81+
# Gate the spec on PRs: `nix flake check`, `just check` (nix-fast-build over
82+
# .#checks) and selfci all build the flake checks. Reuses the derivation
83+
# above, so this adds no duplicate compilation.
84+
checks.spec = config.packages.spec;
3585
};
3686
}

spec/.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
_build/
2+
*.agdai

spec/README.md

Lines changed: 73 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,89 @@
11
# hydra-formal-specification
22

3-
Agda specification for the hydra-protocol.
3+
Literate-Agda + Typst specification for the hydra-protocol.
4+
5+
For an overview of the approach (the trust story, extraction pipeline and
6+
agreement tests), see the developer docs page `docs/docs/dev/agda.md`
7+
("Agda formalisation" on the docs site).
8+
9+
The prose, math and figures are written in [Typst](https://typst.app); the source
10+
files are literate Typst (`.lagda.typ`) so that Agda code blocks are type-checked.
11+
Code fences render as follows (see `src/template.typ`):
12+
13+
- ` ```agda ` blocks are type-checked by Agda **and** shown in the PDF (collected
14+
into the Agda appendix, grouped under the section they support; the body prose
15+
links there in place).
16+
- bare ` ``` ` blocks (module declarations, imports, proof bodies whose
17+
statements are shown separately) are type-checked but hidden.
418

519
## Building
620

7-
To produce the specification PDF in `result/`:
21+
To produce the specification PDF (via `nix build .#spec`):
822

923
```
10-
nix build
24+
nix build .#spec
1125
```
1226

27+
The PDF is written to `result/hydra-spec.pdf`.
28+
1329
## Developing / writing
1430

15-
In a nix shell (`nix develop` or using `nix-direnv`) you can type check:
31+
In a nix shell (`nix develop` or using `nix-direnv`) you can type-check the Agda:
32+
1633
```sh
17-
agda src/Hydra/Protocol/Main.lagda
34+
agda src/Hydra/Protocol/Main.lagda.typ
1835
```
1936

20-
or build the PDF iteratively:
21-
``` sh
22-
shake
37+
or build the PDF iteratively with the build script (Agda typecheck +
38+
`check-refs.sh` + `check-trust-ledger.sh` + Typst render + a tooltip
39+
post-process, `annotate-notation.py`: invisible hover annotations giving the
40+
definition of each notation symbol; skip it with `ANNOTATE_NOTATION=skip`):
41+
42+
```sh
43+
./build.sh
2344
```
2445

25-
with the specification PDF available in `_build/hydra-spec.pdf`.
46+
with the specification PDF available in `_build/hydra-spec.pdf`. The script
47+
needs `JULIAMONO_FONT_DIR` (the code font); the nix dev shell exports it, so
48+
run it from `nix develop` (or set it to a directory containing the JuliaMono
49+
TTFs). Typst >= 0.14.1 is required (0.14.0 emits the PDF's named-destination
50+
table unsorted, typst#7248, which leaves internal section links dead in
51+
spec-compliant viewers); the dev shell and `nix build .#spec` provide a
52+
suitable typst.
53+
54+
## Keeping the spec and the code in sync
55+
56+
The machine-checked chain is: the `*Valid` bundles in the literate spec imply
57+
the decidable reference checkers (`Reference.agda`, proved in
58+
`ReferenceBridge.agda`), the checkers are extracted to Haskell
59+
(`hydra-agda/`, committed output regenerated by `hydra-agda/regenerate.sh`),
60+
and the extracted checkers are differentially tested against the real
61+
validators (`hydra-tx` `HeadValidatorAgreement`) and node logic (`hydra-node`
62+
off-chain agreement tests). Two flake checks gate all of this at PR time:
63+
`checks.spec` (the full spec build, including the consistency lints) and
64+
`checks.hydra-agda-generated` (fails if the committed extraction output is
65+
stale against the `.agda` sources).
66+
67+
When you change something, the places to update:
68+
69+
- **On-chain validator behaviour** (`hydra-plutus`, `deposit.ak`): update the
70+
transaction's section and `*Valid` bundle in
71+
`src/Hydra/Protocol/OnChain.lagda.typ`; if the changed condition is
72+
decidable, mirror it in `Reference.agda`, prove the bundle implies it in
73+
`ReferenceBridge.agda`, run `hydra-agda/regenerate.sh`, expose it in the
74+
`hydra-agda` shim, and extend the `HeadValidatorAgreement` family. New
75+
mocks/postulates in the bridge fail `check-trust-ledger.sh` until its
76+
trust-ledger table and `EXPECTED_*` lists are updated.
77+
- **Off-chain node behaviour** (`hydra-node` `HeadLogic`): update the §6
78+
handler arms in `src/Hydra/Protocol/OffChain.lagda.typ` (and the protocol
79+
figure if the visible behaviour changed); mirror decidable decisions in
80+
`OffChainReference.agda` and bind them against the real handler in the
81+
`hydra-node` agreement tests.
82+
- **Datum/state shape**: `HeadDatum` in `OnChain.lagda.typ` plus the diagrams
83+
(`src/diagrams.typ` `state-fields`); `check-refs.sh` fails on constructor
84+
drift, but the per-state field *tuples* it does not compare, so eyeball the
85+
rendered figures.
86+
- **Discrepancies**: record anything found between the spec and the
87+
implementation in the PR description or an issue; the differential layer's
88+
trusted base (injected mocks and encoding/faithfulness postulates) is
89+
enumerated and drift-checked by `check-trust-ledger.sh`.

spec/Shakefile.hs

Lines changed: 0 additions & 42 deletions
This file was deleted.

0 commit comments

Comments
 (0)