Use issues for bugs and design discussion, and pull requests for changes. Report a suspected vulnerability privately, as SECURITY.md describes.

Find your way around§

blueprint-platform/   the roc-blueprint platform that Blueprint.roc apps use
  *.roc                  setting types, checked values, lowering to the Spec
  host/, build.zig       Zig host, built into targets/<target>/libhost.a
  targets/               linker inputs, none committed: libhost.a is built, the rest fetched (see its README)
blueprint-core/          roc-blueprint-core: Spec, Provider contract, validation, Steps, Value, codec
  Project.roc            pure normalization, references and provider capability checks
  Realise.roc            pure planning of a request: what the Core supplies, and what it asks of a provider
  Sbom.roc               pure SBOM inventory; writes SPDX 3.0.1, SPDX 2.3 and CycloneDX 1.7, reads SPDX 3.0.1, 2.3 and 2.2 and its own CycloneDX
  Spdx3.roc, Spdx2.roc   the SPDX 3.0.1 and 2.x rules, held to the schemas pinned in tests/sbom/schema/
  CycloneDx.roc          the CycloneDX 1.7 rules, held to the schema pinned in tests/sbom/schema/
  fuzz/                  roc-fuzz targets: spec-parse, spec-round-trip, project-validate,
                         lock-parse, lock-snapshot-parse, lock-snapshot-round-trip,
                         lock-snapshot-project, sbom-render, sbom-read, build-record-parse,
                         build-model-compare, nix-native-decode, nix-lock-decode
blueprint-nix/           importable pure Nix provider (depends only on blueprint-core)
  NixProvider.roc        shared pure request planning and flake rendering
  Locks.roc              Nix pins <-> the Lock: Sources plus a "nix" hint
  tests/                 Spec fixtures and golden flakes
blueprint-cli/           the blueprint CLI (basic-cli + weaver), which executes provider plans
  Store.roc, Download.roc  the per-user store of downloads and the HTTPS client that fills it
  StoreRecord.roc        the record the store keeps beside a tree, read and written without effects
  Bootstrap.roc          identity of the compiler archive embedded in each CLI
  compilers.roc          Bootstrap and StoreRecord as a package for build tooling and tests
  Published.roc          what a release publishes as the digest of an asset, asked by update
fixtures/consumer/       independent consumer of the Spec and Nix packages
fixtures/overlays/       overlay flakes for scripts/test_scenarios.roc
examples/all-settings/   environments, sources, scoped overlays, tasks and Raw
examples/composition/    imported pure modules: reusable task settings and a program's recipe
examples/artifacts/      runnable source/dependency/workflow example and its Roc scripts
examples/extensions/     Custom blocks; CI checks blueprint refuses them clearly
scripts/                 Roc scripts, run from the repository root; their modules are in scripts/src/
  test.roc               local CI test groups; runs the test_*.roc suites
  link_inputs.roc        fetch or check the platform's linker inputs
  bundle.roc             bundle core, the platform and the reporting app into dist/ and smoke-test them
  embed_compiler.roc     stage and verify one target's compiler archive for embedding
  test_bootstrap.roc     test embedded compiler installation, repair and concurrency
  build_release.roc      cross-build the CLI, the SBOM tool and the smoke-test program for each released system
  smoke_binary.roc       run a built CLI with no Roc and no Python of its own; with --no-nix, with no Nix either
  fuzz.roc               replay or fuzz the roc-fuzz targets
  release_notes.roc      render a release's notes from docs/releases/
  package_examples.roc   archive the examples with a release's platform URL
  package_sbom_fixtures.roc  archive the SBOM fixture set with its case contract
  test_release.roc       test that archive against the bundles, served from localhost
  docs_snippet.roc       write the part of the manual that names the latest release
  check_version.roc      check a release's version before building and against the public API after
  package_docs.roc       archive the rendered site
  release_assets.roc     gather a release's assets and check the names other repositories depend on
  test_lock_snapshot.roc  check the official lock snapshot consumer against the vendored schema and profile
  test_builds.roc, test_workflows.roc   real-Nix build and workflow suites on scripts/src/BuildHarness.roc
  reproducible_probe.roc  compare a producer on independent CI hosts and synthetic CLI patch builds
docs/                    the manual: index.adoc includes one chapter per top-level .adoc file
  releases/              each release's notes, included by releases.adoc
link-inputs.lock.json    the linker-input release the platform links, pinned by content
flake.nix                builds blueprint with the pinned Roc; the development shell; the release cross-compiler

The local platform and CLI share blueprint-core, including Project.validate. The Spec wire format is major 3; older platform bundles are not compatible. The architecture, terminology (Spec, Provider, Lock, stages) and invariants are defined in the architecture chapter; keep it in sync with changes.

Keep capabilities provider neutral§

Provider neutrality is a lasting design constraint, including for a future executor developed within Blueprint. Review shared-contract changes against it:

Protocol changes need a shared semantic reason and the existing versioning rules. They must not make every provider adopt one ecosystem’s native format. Production support for a second provider and general resolver composition remain follow-up work tracked in the additional-provider follow-up.

Review changes against the ownership table in the architecture chapter. Scripts own build algorithms; the pure core owns validation and plans; the host owns effects; providers own native execution and caches. Identity fields must say whether they identify input content, a prepared build or measured output. Do not reconstruct a build graph from independent JSON reports when a shared plan/result type can serve the interface.

Set up§

Full development and CI require x86_64 Linux with Nix and flakes enabled.

nix develop

gives two Roc compilers from roc-overlay, Zig 0.16 and the programs the scripts start: coreutils, git, curl, tar, gzip, zip, unzip and gh. On Linux it also supplies Bubblewrap, util-linux and Python for isolated runner tests and a restrictive-umask launcher. Nothing else uses Python: the product, the reporting app and every conformance check run in Roc. roc is the product compiler, the nightly in blueprint-cli/compiler-version: it builds and tests the CLI, the packages and the platform, and a released blueprint embeds it. roc-stable is the nightly rocStableTag names in flake.nix: the scripts in scripts/ start with #!/usr/bin/env roc-stable and run on it, so they keep working while the product moves to a newer nightly. The scripts reach the product compiler through ROC, or roc on PATH. This shell is the one definition of the toolchain: every CI job runs its scripts through nix develop -c, so what passes in the shell is what CI runs. blueprint is not in the shell, so entering it never compiles the CLI; build it as below, or use nix run ..

The shell stages this host’s pinned compiler archive as the ignored file blueprint-cli/compiler-archive. Roc imports its bytes into the CLI at build time. Outside the shell, stage an archive first with scripts/embed_compiler.roc <system>; the script checks its size and digest against Bootstrap.roc. Keep the complete archive: macOS Roc needs the darwin/ sysroot beside its executable.

blueprint-cli/main.roc uses the released basic-cli platform by URL. Roc downloads it on first use outside Nix; the Nix blueprint package fetches the same archive by hash, so its sandboxed build needs no network.

Build and test§

scripts/link_inputs.roc fetch             # the musl runtime files the platform links
(cd blueprint-platform && zig build)      # libhost.a or host.lib for all five hosts
roc test blueprint-core/main.roc      # Spec round trips and format tests
roc test --opt=interpreter blueprint-cli/main.roc # CLI, provider and core expects
roc build blueprint-cli/main.roc --output=./blueprint
(cd examples/all-settings && ../../blueprint run --help) # try the CLI
scripts/test.roc                            # all local CI test groups
scripts/test.roc unit cli                   # or only some groups: static, unit,
                                            # cli, scenarios, builds, package, glue, fuzz
scripts/fuzz.roc 300                        # fuzz each target for 5 minutes

The CLI unit tests use interpreter mode because the pinned compiler’s native test backend crashes on the large embedded byte constant. Integration tests run the compiled executable.

The .roc files in scripts/ are Roc programs; they start other programs by argument list, not through a shell. Run one from the repository root with the pinned roc on PATH, or as "$ROC" scripts/test.roc.

The platform’s linker inputs are not committed. scripts/link_inputs.roc fetch downloads the release pinned in link-inputs.lock.json into .cache/link-inputs/, verifies it and installs the files beside libhost.a; nothing that evaluates a Blueprint.roc against the local platform links without them. scripts/link_inputs.roc check verifies what is installed without the network. See blueprint-platform/targets/README.md.

scripts/test.roc runs the repository’s CI test groups. Its groups are static (formatting; no tracked object file, archive or import library; the CLI’s use of the Provider contract; the compiler downloads Bootstrap.roc records, one for each system the CLI runs on; each workflow’s permissions and checkouts; the release notes chapter against the notes files; the flake’s list of core modules and its version), unit (the platform’s, the scripts', the CLI’s and the consumer’s expects, the configuration fixtures and the Roc the manual shows), cli, scenarios (test_scenarios.roc, test_roc_packages.roc and the golden flakes parsed by Nix), builds (test_builds.roc and test_workflows.roc), package (the flake’s package and development shell, the packaged CLI building an artifact with only Nix on PATH, and a dry run of what a release ships: core and the platform bundled and smoke-tested, the examples archived and the archive tested; see The two bundles), glue (scripts/check_glue.roc: the host’s committed roc_platform_abi.zig is what roc glue writes for the platform with the product compiler; scripts/check_glue.roc --update rewrites it) and fuzz (each target’s committed corpus, replayed, which takes seconds; .github/workflows/fuzz.yml fuzzes each target for five minutes every week, starting from the inputs earlier runs found, which it keeps in the Actions cache through FUZZ_CORPUS, and uploads any crashing input). scenarios and builds are the real-Nix suites, split into two groups of about the same length so that CI runs them as two jobs at the same time. Two additional CI jobs run scripts/reproducible_probe.roc on independent Ubuntu 24.04 and 22.04 hosts. One uses the release binary; the other compiles a synthetic next-patch CLI with the same runner pins. Each builds and rebuilds the same selected-input producer. A dependent job compares their actual derivation, outputs and locked-source provenance. scripts/test_config.roc runs the compiler on generated Blueprint.roc fixtures: the platform’s own rules, which only a compiler run can show (a setting given twice or not at all, in Lower.roc, and each checked name’s from_quote), a few core rules and bounds as witnesses that shared validation surfaces at compile time, and that settings composed from an imported module emit the same Spec as inline ones. Every other rule of Project.validate has an expect in blueprint-core/Project.roc. The bundle gate repeats a few of the fixtures against the served platform, plus the all-settings example. It then compiles the manual’s Roc (scripts/src/DocSnippets.roc): every [source,roc] block of docs/ and docs/releases/, and the first program of the Get started chapter, against the platform in the working tree; see Write the manual. scripts/test_cli.roc exercises the built CLI with and without a configuration, checks validation and help/version handling, and records Nix argv to verify shell selection and task arguments without entering a shell. It holds what only a run can show: the order of checks before any effect, how often the configuration is evaluated, exact provider argv and error text. Rules that a pure expect in blueprint-core or blueprint-nix already holds are not repeated there. The all-settings integration tests separately run tasks through real Nix. scripts/test_scenarios.roc writes projects and runs them through the real CLI and real Nix: the all-settings and extensions examples, noncommutative overlays in both orders with inheritance, an unselected overlay’s native missing-package failure and a declared overlay that throws if evaluated, system-scoped tools on Linux and macOS with the package-target rejection of an unscoped one, renamed commands, and a task that names a script by path, as scripts/check_all.roc, which starts by its roc-stable shebang on a platform RocPackages supplies and imports a module beside it, and zig-overlay and rust-overlay as overlays, each supplying an exact release named in a quoted attribute, and configured tools: a fixture overlay’s package says which options it was built with, and rust-overlay evaluates a toolchain with two targets and a component. It also runs nix develop --debug on a staged flake and requires Nix’s own report that its bash comes from the locked package source, with no registry lookup,. A project it writes pins its inputs to the revisions in fixtures/consumer/inputs.lock and flake.lock, and its Blueprint.lock is read-only and compared after every step. fixtures/consumer/main.roc holds the staged bytes and supplied-lock preservation as expects. Parse fuzzing also checks successful semantic normalization for idempotence. SBOMs are SPDX 3.0.1 JSON-LD, with SPDX 2.3 and CycloneDX 1.7 renderings of the same inventory for consumers that read only those, and conformance is checked in Roc, with no other validator. blueprint-core/Spdx3.roc, Spdx2.roc and CycloneDx.roc hold the classes or objects, members, enumerations and prose rules of each format, and scripts/src/SbomTest.roc holds them to the digest-pinned schemas in tests/sbom/schema/ (the published SPDX 3.0.1 JSON schema, the SPDX 2.3 schema and the CycloneDX 1.7 schema): every class, member and enumeration the validators accept is the schema’s own. The same expects require that each committed fixture in tests/sbom/fixtures/ is what the renderers write now (regenerate them with scripts/test_sbom.roc fixtures tests/sbom/fixtures), that all three renderings of a fixture read back to one package graph, and that the standalone tool’s committed inputs (input/, source-import/), the case contract (CONTRACT.md) and the file list of the release asset (scripts/src/SbomFixtures.roc) are what the suite states (regenerate the first three with scripts/test_sbom.roc inputs, source-import and contract). scripts/test_sbom.roc binding then runs the built scripts/sbom.roc against read-only inputs: the native case, and the source supplier import’s accepted targets, bound renderings and refusals as source-import/expected.json states them. The sbom-render fuzz target generates inventories with arbitrary text and graphs: each must render to valid, canonical SPDX 3.0.1, SPDX 2.3 and CycloneDX 1.7, read back from each to exactly its own packages and relationships, with CycloneDX’s standard fields (dependencies, scopes, hashes, purls, licenses, suppliers) derived from the inventory, and the validators must reject a rendering once one of a list of SPDX 3.0.1 or CycloneDX rule violations is injected into it. It also strips Blueprint’s properties from the CycloneDX rendering, which must then import as plain CycloneDX to a projection of the inventory, and adds files to the SPDX renderings, which must read to the same package graph with the files counted as gaps. sbom-read mutates the fixtures of every format, including what Syft and Trivy write (tests/sbom/fixtures/upstream/): reading must never crash, what it accepts must be a sound package graph, and it must import as an upstream SBOM unless the import is refused for a developer path, a credential, its size or a subject digest that is not a SHA-256. scripts/test_builds.roc runs real sandboxed builds of artifacts, dependencies and sources, including host-file and TCP isolation with positive host controls, fail-closed runner checks, exact argv, immutable locks, freshness and relocation. It also checks what Nix copies from the project (bytes, the owner’s execute bit, raw names, exclusions, refused symlinks and special files) and that an undeclared tool is not found. The exact standalone runner’s argv, input and output checks run in an isolated Linux test harness; an unisolated invocation must refuse execution before user code runs. Runtime namespace witnesses are private to each invocation and do not enter the derivation. Stubbed CLI tests verify their contents, cleanup and separation from stable recipe bytes. scripts/test_workflows.roc runs real ordered task/build workflows, nested repetitions, failure stops, snapshot/dependency freshness, repeated locked-source verification, whole-closure preflight and immutable authority, including out-of-tree layouts, and then a temporary copy of the complete artifacts example. Both are Roc scripts on the harness in scripts/src/BuildHarness.roc, and the tasks and builds of their fixtures are Roc scripts run as roc-stable through RocPackages. Their inputs are pinned in fixtures/consumer/inputs.lock and fixtures/roc-inputs.lock.json; one first step fetches them and every later Nix call is recorded and run with --offline. See fixtures/builds/README.md. scripts/test_update.roc checks local-source preflight, authority observation, concurrent publication, named updates and what an unsupported lock is told. scripts/test_isolation.roc checks what a build stages about its caller: the namespace identities, the CLI’s own path as the runner, and the refusals when either cannot be used. Both put a failing python3 on PATH: the CLI itself must not use a host Python. scripts/test_programs.roc runs projects whose environments fetch programs and expect tools of the machine. Nothing selects how they run but their own Blueprint.roc: one with no Nix tool must never start the nix stub. The downloads come from a server the script runs on the loopback address, the store is a directory of the test’s own, and the lock entries are written by the test. It checks what is kept in the store and under which names, that a second run makes no request, that a stored artifact with other bytes is removed with its tree and fetched again, two runs at once from an empty store, every refusal (a missing expected program, a program the lock does not hold or holds under other URLs, another length or digest, each URL’s failure, an archive with a link out of its tree), the caller’s own shell, and parity: the same task with one Nix tool added prints the same from the same store file, and only then starts Nix. Its last cases let blueprint update write the lock: with no nix on PATH for a project that only fetches, with nothing asked when nothing changed, for a named program alone, after a URL list changed, and for release assets. A download URL must be https://, with one exception made for these tests: http://127.0.0.1:<port>/, which no other machine can serve. A loopback URL of the shape /<owner>/<repo>/releases/download/<tag>/<asset> is a release asset, and the same server describes its release at /repos/<owner>/<repo>/releases/tags/<tag>, as GitHub’s API does for a GitHub URL. That is the whole seam: no variable or flag redirects the question. Through it a digest computed here is compared with a published one, another system’s is recorded unfetched, a published digest that differs stops the update, a question that is refused or answered with something else leaves every digest computed and says so once, and a download missing for one system says how to declare that. It ends with blueprint requirements, asked with no nix, no lock, an empty store and no server: the report is the same, a declared and unused Nix source requires no provider, and nothing is written. These are Roc scripts, run from the repository root with ./blueprint built; ROC names the compiler. They replace every tool the CLI runs with one program, scripts/stubs/tool.roc, which each test builds once and installs under several names (roc-record, roc-wire, roc-probe, nix, guix, python3, readlink). A copy decides what to be from its own file name and how to behave from STUB_* variables, and records each invocation as a file holding one line of JSON in <name>-calls/ under its working directory. Its header lists what each name does. scripts/src/CliHarness.roc holds what the three tests share. scripts/test_roc_packages.roc is a Roc script, like link_inputs.roc. It runs the real CLI and Nix with a Roc package cache of its own in a temporary directory, and refuses to run if that directory lies inside ~/.cache. A task’s bundles are downloaded by blueprint itself from the releases their URLs name. It checks publication into an empty cache, that a second run writes nothing, that an existing package is left alone and an incomplete one replaced, that failures leave nothing behind, that a project with no build locks no bundle and its task still downloads them, and that a sandboxed build resolves its bundles from the lock with no network while the same build without one of them does not. Normal execution tests explicitly initialize authority with update first. The complete artifacts example is executed in a temporary copy by the workflow suite (scripts/test_workflows.roc).

Keep fuzz coverage with the code§

Every reader of bytes that Blueprint did not write in the same process has a fuzz target in blueprint-core/fuzz/: committed authority (the Spec: spec-parse, spec-round-trip and project-validate; the Lock: lock-parse), reports it reads back (the lock snapshot: lock-snapshot-parse, lock-snapshot-round-trip and lock-snapshot-project; SBOMs: sbom-read and sbom-render), native tool output a provider decodes (Nix derivation, build and dev-shell JSON: nix-native-decode; the native lock graph and the Nix pins in the Lock: nix-lock-decode) and local records (build records: build-record-parse and build-model-compare).

The job with no Nix§

Every suite above runs where Nix is installed, and replaces it with a stub where it must not be started. One job of .github/workflows/ci.yml runs on machines that have no Nix at all: no-nix, on ubuntu-latest, ubuntu-24.04-arm, macos-latest, macos-15-intel and windows-latest, with the binaries and platform hosts the build job produced and no step that installs anything. It runs dist/smoke-<system> --no-nix dist/blueprint-<system>, the NoNix mode of scripts/src/SmokeBinary.roc, which:

The same job then runs the cross-built SBOM tool of its system, dist/blueprint-sbom-<system>, over tests/sbom/fixtures/input (.github/actions/sbom-tool): inspect must reproduce the committed inventory, bind writes the sample artifact’s sidecars and check holds them, all with no Roc on the machine.

It reaches GitHub for the nightly and the Roc packages, and asks GitHub’s API once for the nightly’s published digests; a refusal there is a notice, not a failure. To run it on a machine that has Nix, hide Nix from PATH:

roc build scripts/smoke_binary.roc --output=/tmp/smoke
env PATH=/usr/bin:/bin /tmp/smoke --no-nix ./blueprint

Write the manual§

The manual is the AsciiDoc in docs/. index.adoc is a book that includes one chapter per top-level .adoc file; .github/workflows/docs.yml renders it as a site and a PDF with the shared build-docs action, on every pull request, and deploys the site from main. The action is pinned to a reviewed roc-automation main revision; the renderer is released as tools-0.6.0. flake.nix checks the released renderer binary’s SHA-256 for each supported host. Rendering uses the native Roc utility. API generation uses this repository’s pinned Roc compiler.

To render the same outputs locally, inside nix develop:

scripts/docs_snippet.roc    # docs/generated/, from the latest release; needs a logged-in gh
renderer=$(nix build .#roc-docs --no-link --print-out-paths)/bin/roc-docs
"$renderer" build --workspace "$PWD" --pdf-filename roc-blueprint.pdf \
  --pdf-image-dpi 240 --site-url https://lukewilliamboswell.github.io/roc-blueprint/ \
  --docs-version main --api-entrypoint blueprint-platform/main.roc \
  --roc-command "$(command -v roc)"

The site is .docs-out/site/ and the PDF .docs-out/roc-blueprint.pdf; neither is committed. Both workflows use 240 DPI for PDF image preparation so the architecture diagram retains detail when zoomed. Publication links use explicit URLs; this action and renderer pair uses site-url for publication links and Markdown discovery.

Keep PSV table cells on complete physical lines. HTML honours the declared column proportions; this renderer release’s PDF sizes earlier columns to their content and gives the last column the remaining space. Keep long explanations together: the overview combines each source and its version pin, and the configuration checks use labelled paragraphs. Stage responsibilities use separate input/output, execution and invariant tables. Review both outputs after changing a table; column proportions alone do not establish that its PDF is readable.

Stable explicit anchors preserve links when a heading’s wording changes. Invariant headings use words before their numbers because numeric-leading headings exceed this release’s native inline profile. The historical use-this-release-2 release anchor remains explicit. Prepared figures replace render-time Mermaid and JPEG conversion, with their source and asset digests recorded below. These are authoring changes, rather than renderer patches.

Native images are non-interlaced RGB/RGBA PNGs. The manual’s illustration is converted from the original JPEG; its source hash and conversion command are in docs/images/blueprint-illustration.provenance.json. The architecture flow uses the checked-in published SVG and its prepared PNG, with provenance in docs/images/blueprint-flow.provenance.json. Its Mermaid source stays in architecture.adoc; docs/prepared-diagrams.json binds its exact source and PNG hashes. Update the figure and registration together when editing the Mermaid block. These prepared assets let ordinary documentation builds run without a browser, image converter or diagram tool.

The manual describes the tool as it is: one recommended way to do each thing, in the present tense. What changed between versions belongs in the release notes, and what is unfinished in an issue.

The Roc in the manual is compiled by scripts/test.roc unit, so a block must be one of five things: