Wheeler

WIP-0009: Wheeler package and build system

FieldValue
StatusImplementing
OwnersWheeler package, build, compiler, security, and release maintainers
Created2026-07-17
Updated2026-07-17
AreaPackages, modules, builds, registry, tooling
Depends onWIP-0007, WIP-0008
SupersedesNone
Superseded byNone

Summary

Wheeler will have its own package manager and build system, written in Wheeler. The wheeler command manages workspaces, dependency resolution, compilation, testing, documentation, native lowering, content-addressed packages, and repositories. It ships with the native recovery release and replaces Gradle after the WIP-0008 cutover.

The tool uses familiar ideas from modern package systems: one command, declarative manifests, exact lockfiles, reproducible builds, workspaces, repositories, and hermetic tests. It is not a Cargo wrapper. Wheeler packages must preserve the language's full typed IR profile, including ownership, inverse and history classes, coherent permutations, quantum regions, adjoints, proofs, effects, target capabilities, limits, and native ABI data.

A source manifest is declarative data. It is not an unrestricted build script. Build extensions are versioned Wheeler tool packages that run with explicit capabilities, bounded inputs, declared outputs, and no ambient file or network access. Resolution and artifact creation are deterministic. Credentials, provider sessions, host paths, and changing target calibration never enter package or lock identities.

Motivation

Keeping Gradle would leave a Java dependency after Java was supposed to be gone. Replacing Gradle with Cargo would move Wheeler's build graph under another language's package rules and bootstrap chain. Shell scripts and ad hoc downloads would make dependency identity, effects, and reproducibility hard to verify.

Wheeler also needs package rules that general host tools do not model:

The package system is core language infrastructure. It is also a major Wheeler acceptance program.

User model

A repository contains one workspace manifest and one or more packages:

wheeler.workspace.yaml
wheeler.package.lock.yaml
build/
  wheeler-compiler/
  wheeler-runtime/
compiler/
  wheeler.package.yaml
  src/
runtime/
  wheeler.package.yaml
examples/
  wheeler.package.yaml

The command surface begins with:

wheeler new
wheeler check
wheeler build
wheeler test
wheeler run
wheeler doc
wheeler format
wheeler package
wheeler publish
wheeler fetch
wheeler vendor
wheeler clean
wheeler explain

Commands operate on the workspace graph by default and accept explicit package, target, profile, offline, locked, and capability-policy selections. Ordinary commands do not execute provider hardware. Hardware tests require an explicit named target grant and remain outside deterministic default CI.

Goals

Non-goals

Names and files

wheeler is the Wheeler toolchain driver. wheeler run <package-or-artifact> executes a selected package target or verified artifact.

wheeler.package.yaml is a UTF-8 Wheeler package manifest. wheeler.workspace.yaml is the optional workspace manifest. wheeler.package.lock.yaml is the generated canonical resolution. .wpk is the canonical package archive and wheeler.workspace.plan remains the canonical binary build plan. Names and suffixes are Wheeler contracts and do not alias host package formats. The retired extensionless metadata names have no fallback lookup. One package graph does not need two front doors.

Generated target artifacts and default package archives live below <repository>/build/<workspace-member>/. wheeler clean owns that tree. A directly invoked member discovers the adjacent workspace and uses the same group, while a standalone package uses its own build/. A package-local vendor/ is different: a standalone offline build generates this content-addressed dependency input closure, and Git ignores it. The recovery workspace keeps exact locks and canonical member sources. Workspace commands reconstruct matching dependency archives in memory. An exported air-gap bundle may still carry a verified vendor set. The source repository doesn't need to contain that generated closure.

A package identity contains:

(namespace, name, version, source_identity, manifest_identity)

A resolved dependency also fixes its archive content hash and registry or path-source identity. A build identity includes the complete lock graph, compiler artifact, language profile, options, declared environment, build-tool artifacts, target triple when applicable, and platform ABI.

Manifest language

Package metadata uses a closed, canonical YAML 1.2 profile. The sectioned shape borrows the useful part of pyproject.toml: one clear project header followed by targets, dependencies, and capability authority. Schema 1 deliberately leaves feature selection out until the resolver can bind it into identities end to end. It does not borrow executable build hooks or arbitrary extension tables. Unknown keys fail instead of becoming implicit extension namespaces.

schema: 1
package:
  name: "wheeler.compiler"
  version: "0.1.0"
  profile: "bootstrap-1"
targets:
  - kind: "tool"
    name: "compiler"
    root: "src/main/wheeler/MinimalCompiler.w"
    module: "wheeler.compiler.main"
    sources:
      - "src/main/wheeler/MinimalCompiler.w"
      - "src/main/wheeler/compiler"
      - "src/main/wheeler/lexer"
    test: false
  - kind: "tool"
    name: "compiler-laws"
    root: "test/compiler_laws.w"
    test: true
dependencies:
  - kind: "build"
    name: "wheeler.bytecode"
    version: "^0.1.0"
capabilities:
  - name: "build.read"
    path: "src/**"
  - name: "build.write"
    path: "build/**"

The target kind set is closed: deployable, library, and tool. The test field selects a runnable target. It is not a separate kind. A later schema may add named feature lists. Schema 1 rejects features. Ignoring an option that could change reproducibility is unsafe. Future features may enable optional dependencies or declared target facets. They cannot remove checks, grant capabilities, select credentials, or run code. Unknown keys fail unless a later schema defines them.

Wheeler accepts a closed YAML profile. It allows one UTF-8 document, LF endings, two-space indentation, plain mapping keys, block mappings and sequences, quoted strings, canonical decimal integers, booleans, and full-line comments.

The parser rejects duplicate keys, tabs, implicit scalar types, nulls, floats, timestamps, anchors, aliases, merge keys, tags, directives, flow collections, block scalars, multiple documents, and unknown fields. Canonical output fixes key order, unordered-list order, escaping, normalization, and one final newline. Standard YAML 1.2 readers can inspect the result, but Wheeler implements only this bounded subset.

Logical package paths use /, reject traversal and absolute roots, and stay case-sensitive on every host. A modular source selector may name one file or one directory. Directory expansion includes only physical nonsymlink .w files, sorts logical paths, rejects empty or oversized results, and must include the declared root. Selection never expands outside the package boundary.

Modules and visibility

WIP-0007 modules live inside packages. A package declares roots and exports. Imports name modules, not files. Resolution maps module names through the package manifest and locked dependencies. Source code cannot walk directories or import undeclared relative host paths.

Package boundaries participate in visibility and effect checking. Public APIs include type, effect, reversibility, coherent eligibility, affine-resource, and target-requirement signatures. A package's API identity changes when one of those observable contracts changes even if method names do not.

Cyclic module imports and cyclic normal dependencies are rejected. Development and build-tool dependency edges have separate acyclic phases so tools cannot observe outputs they are currently producing.

Versions, features, and resolution

Versions use a specified semantic version grammar for human compatibility policy. Exact content hashes, not version text alone, establish fetched identity. The resolver is deterministic for a registry snapshot and lock input.

Feature selection is additive and namespaced. Features cannot silently remove checks, weaken reversibility, grant capabilities, or select credentials. Target-specific optional code uses explicit profile and target predicates evaluated against declared build inputs, not ambient host probing.

Resolution:

  1. parses all workspace manifests.
  2. verifies names, versions, source identities, and dependency phases.
  3. obtains a signed or content-addressed registry index snapshot unless offline.
  4. selects one deterministic solution under the version and profile constraints.
  5. records every package, content hash, feature, source, and relevant schema identity in wheeler.package.lock.yaml.
  6. verifies the complete graph before fetching or building code.

A locked build does not re-resolve. If a locked archive disappears or has different bytes, the build fails. It does not choose a convenient replacement.

The initial resolver may require one version of each package identity in a final graph. Multiple-version support requires explicit type and resource identity rules and is deferred until demonstrated necessary.

Lockfile

wheeler.package.lock.yaml is generated canonical YAML data. It is committed for applications, tools, and the Wheeler recovery workspace. Libraries may commit it for development reproducibility without forcing consumers to use that graph.

schema: 2
root: "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"
packages:
  - name: "wheeler.core"
    version: "0.1.0"
    repository: "89abcdef0123456789abcdef0123456789abcdef0123456789abcdef01234567"
    archive: "fedcba9876543210fedcba9876543210fedcba9876543210fedcba9876543210"
    manifest: "76543210fedcba9876543210fedcba9876543210fedcba9876543210fedcba98"
    dependencies: []

The lock uses the same closed YAML profile and canonical writer as the manifest. It is data for review and merge tooling, but the resolver remains its only author. Hand-edited hashes still fail structural and graph verification.

The lockfile records:

It excludes credentials, tokens, home directories, temporary paths, clocks, random seeds not explicitly semantic, provider queue state, and mutable calibration data.

Lockfile updates are atomic. A failed resolution leaves the prior lockfile untouched. Rerun the resolver from reviewed manifests to resolve merge conflicts. Do not hand-edit package hashes.

Package archive

A .wpk archive is a canonical, bounded, content-addressed container. It includes:

Native images may be distributed as target-qualified attachments or separate derived packages. Their identities point back to canonical .wbc. A native image cannot replace missing portable semantics.

Archives reject duplicate paths, traversal, links escaping the package root, special devices, unknown required records, excessive expansion, and noncanonical member order. Extracting an archive is not required to verify or compile it.

Build graph

wheeler converts the locked package graph into a deterministic build plan. Nodes declare:

The plan is content-addressed. Cache hits are accepted only after complete build-input-key matching plus output hash and schema verification. XDG-local and remote caches are equally untrusted stores. Signatures may establish provenance but never replace local structural verification.

Independent nodes may execute concurrently. Semantic output order, diagnostics, archive members, and lock updates are reduced in canonical graph order instead of task completion order.

Build tools and capabilities

There are no ambient install scripts. A formatter, parser generator, documentation renderer, native linker driver, or other extension is a locked Wheeler tool package. The build engine launches it with a capability object containing only declared logical inputs and output destinations.

A tool cannot read the workspace, home directory, environment, network, clock, random device, target credentials, or prior undeclared output. The package manager owns networked fetch before tool execution. Native system tool invocation, where temporarily required, is a separate named capability with executable identity and normalized arguments in the build record.

Generated files belong to declared output trees and are never silently written into source directories. Promotion of generated source or recovery seeds is an explicit reviewed command.

Tests, examples, and quantum targets

Packages attach the test selector to deployable or tool targets and declare their fixtures. wheeler test builds a deterministic plan from those runnable targets alone. It isolates writable state, seeds declared simulators, and reports in package and source order. An entryless library needs a separate runnable harness.

Test classes include:

Default tests cannot submit live quantum work. A live test names a target capability profile, budget, and credential grant supplied by the host. Its result is operational evidence, not a reproducible package-build input.

Checked-in examples are normal workspace packages with deployable or tool targets. They compile under the same lock graph and profile as user code. No special parser path or target kind exists for examples.

Registry and publication

A registry stores immutable package archives by content identity and a signed append-only mapping from package names and versions to archive identities. Publication never mutates an existing (namespace, name, version) mapping.

Namespaces have explicit ownership and delegation. Clients verify archive hash, manifest identity, namespace authorization, and package limits before resolution. Registry mirrors and offline vendor stores preserve identities. Changing the download location does not change the package.

XDG local repository and artifact cache

The default developer repository follows the XDG base-directory contract. Wheeler does not create /.wheeler, a dot directory directly below $HOME, or a repository in whichever directory the shell happened to enter:

PurposePhysical rootSemantics
Ordered repository policy${XDG_CONFIG_HOME:-$HOME/.config}/wheeler/wheeler.repositories.yamlCanonical alias, repository identity, transport, trust, and lookup order
Durable local publication${XDG_DATA_HOME:-$HOME/.local/share}/wheeler/repositoryImmutable package objects and no-replace name/version mappings for the default local repository
Disposable artifact reuse${XDG_CACHE_HOME:-$HOME/.cache}/wheeler/artifactsVerified build outputs indexed by complete build-input identity
Local journals and quarantine state${XDG_STATE_HOME:-$HOME/.local/state}/wheelerRetry records, quarantine decisions, and bounded GC/accounting state

An XDG variable participates only when it is an absolute physical path, as the XDG specification requires. A missing variable uses the listed fallback. A relative value is ignored with a stable diagnostic instead of reinterpreted beneath the workspace. Repository-policy bytes and selected repository/snapshot identities are deterministic resolver inputs, but their physical XDG location is not. These physical roots never enter wheeler.package.lock.yaml, package identity, plan identity, diagnostics intended for comparison, or canonical provenance. The selected object, repository trust domain, snapshot, build-input identity, and bytes do.

The repository policy is an ordered list, in the useful Conan 2 sense instead of an unordered bucket of URLs. wheeler repository add, remove, enable, disable, move, and list update or print one canonical file atomically. Aliases are unique, repository identities are stable, and physical transports are not identities. Schema 2 also carries a sorted, bounded set of canonical Ed25519 public keys for each trust domain. trust and untrust change that local authority explicitly. sign and verify operate on the current immutable snapshot. Once any key is trusted, configured resolution and exact fetch require a valid current signature. The default list contains the XDG data repository as unsigned local. Command-line repository selections may choose one alias or an explicit ordered subset, but they do not mutate policy behind the user's back.

Unlocked lookup visits enabled repositories in declared order. For one package instance, the first repository with namespace authority and an admissible release owns the lookup. Lower-priority repositories cannot add newer versions to that candidate set. WIP-0022 aliases may select another repository or allow a separate instance. Locks store stable repository and snapshot identities, never aliases, positions, URLs, or XDG paths. A locked lookup contacts only the bound repository or an identity-preserving mirror or cache. Equal-looking bytes in another repository do not authorize a switch.

Without an explicit repository argument, publish targets local, while fetch and unlocked developer resolution use the configured order. An explicit repository may select local or remote transport without changing object identity. Local publication verifies canonical bytes, stages the object and mapping privately, rejects a conflicting (name, version), and publishes with no-replace semantics. Equal publication is idempotent. Atomic visibility is not a durability receipt. WIP-0032 defines any stronger evidence separately from rename(2).

The cache may hold verified outputs produced from workspace source, vendored packages, local publication, recipe builds, mirrors, or independent builders. Origin is provenance, not a cache key shortcut. A hit requires the complete compiler/tool/kernel identities, source and package inputs, profile, target, options, grants, limits that affect bytes, canonical plan node, output schema, length, and digest to match. Wheeler then re-verifies the artifact as untrusted input. Missing, corrupt, divergent, oversized, or schema-wrong entries are deleted or quarantined and rebuilt. Cache location alone never makes an entry a resolver candidate.

Cache insertion is bounded and atomic. Eviction and wheeler cache gc are external maintenance effects over disposable objects and cannot alter resolution, output bytes, diagnostics, or lock data. wheeler clean removes the repository build tree, not vendored inputs, durable local publications, or the XDG cache. Credentials, provider output, coherent quantum state, raw loans, descriptors, registered addresses, and remote keys are forbidden cache payloads.

wheeler publish performs, in order:

  1. locked clean build and test under publication policy.
  2. manifest and public-API validation.
  3. secret and forbidden-file checks.
  4. canonical archive construction.
  5. local archive verification from bytes.
  6. owner signing or authenticated upload.
  7. immutable registry acknowledgement recording.

Yanking marks a version ineligible for new unlocked resolution but does not break existing lockfiles or delete the archive. Security advisories are separately signed metadata and never rewrite historical package bytes.

Hardening and distribution series

The implemented stage-0 archive and locked-build core remains the foundation: declarative manifests, exactly three target kinds, canonical bounded archives, immutable publication, explicit vendor sets, source-bound plans, and no ambient lookup. Before a broad third-party ecosystem, five follow-up WIPs close graph, repository, distribution, native, and image boundaries:

  1. WIP-0022 replaces name-global resolution and flat transitive module candidates with target-scoped package instances, direct aliases, build/target contexts, bounded incompatibility solving, minimal updates, one workspace graph, and scoped capabilities.
  2. WIP-0023 defines repository trust domains, signed snapshots, declarative recipes, RREV/variant/build-input/PREV identities, sealed reproducibility, independent attestations, quarantine, and no-replace publication.
  3. WIP-0024 derives Debian, RPM, and later deployment formats from one canonical install image and typed lifecycle policy.
  4. WIP-0025 defines exact native ABI descriptors, affine foreign ownership, irreversible effects, package-visible providers, and explicit system capabilities without ambient loading.
  5. WIP-0026 defines one loader-native ELF, Mach-O, or PE file containing a verified read-only Wheeler capsule and embedded runtime.

WIP-0028 makes public ownership, region relations, and disposal obligations package API. WIP-0029 binds generic body and closed-instantiation identities. WIP-0030 binds class declarations, defaults, associated members, laws, and selected evidence: ordinary instances come from exact class/principal-type packages, adapters require direct dependency plus explicit activation, and no transitive package changes selection. WIP-0031 binds callable kind, effect row, inverse/adjoint/control evidence, and resource bounds. Together they extend the graph without inventing a parallel artifact or a fourth target kind.

The series preserves these rules:

The shared identity chain is coordinate → RREV → variant → build-input ID → PREV, with separate capsule, native-image, signed-release, and distribution identities. Each arrow adds declared information. None is allowed to erase inconvenient inputs using a callback named packageId() and a hopeful expression.

A public-ecosystem release waits for direct imports, profile-aware and prerelease-aware bounded solving, lock-preserving updates, repository snapshots, namespace authority, PREV uniqueness with independent rebuilds, and native closure checks. Archive integrity is required, but it does not cover the rest of the supply chain.

Self-hosting and recovery

The package manager, build planner, manifest parser, resolver, archive codec, registry client, and command driver are Wheeler packages. The WIP-0007 compiler builds them, and the WIP-0008 native runtime runs them.

A recovery release contains:

A clean recovery build runs with network disabled and Java, Gradle, Rust, and Cargo absent. It rebuilds the current package graph, proves the compiler fixed point, runs conformance tests, and emits the next candidate recovery release.

Stage-0 build tasks seed the initial package manager. Once the Wheeler implementation passes differential lock, plan, archive, and failure tests, delete the stage-0 package logic with Gradle. Do not retain a fallback.

Reversibility, effects, and replay

Dependency resolution is deterministic computation over immutable manifests and an identified registry snapshot. Fetching, cache insertion, output replacement, publication, and yanking are external effects.

A failed build transaction discards private outputs. It does not claim to reverse network transfer or terminal output. Lockfile, archive, and artifact replacement use atomic host effects. Publication acknowledgement is a commit barrier. Recovery reconciles by content and idempotency identity before retrying.

Build-event logs may be replayed for diagnostics and provenance, but replay cannot substitute unavailable package payloads or rerun a publication under an old identity. wheeler clean removes only the selected repository build tree. Explicit cache GC cannot erase a durable local or remote package version.

Security and limits

Every manifest, lockfile, index, archive, graph, tool, and output has byte, count, nesting, path, dependency, feature, step, memory, and expansion limits. Resolution detects adversarial graphs before downloading package bodies where possible.

Package names and paths use fixed Unicode and confusable policies. Archive verification rejects links, devices, duplicate normalized paths, and decompression bombs. Registry and cache inputs are untrusted.

Capability grants are visible in build plans and diagnostics. A dependency cannot grant itself network, process, file, target, or credential authority. Capability expansion requires a root policy decision and changes build identity where it can affect output.

Secrets are opaque host-owned handles and are prohibited from canonical output, lockfiles, caches, traces, and package archives. Redaction is a last defense, not the ownership model.

I/O capabilities and builds

Package capability requests may name WIP-0032 resource domains and operation classes, including file read/write, network connect/listen, direct storage, persistence evidence, RDMA registration/remote access, and target submission.

The Io fabric grants scheduling only. Resource authority remains target- and phase-scoped under root policy. A reproducible build program receives no ambient live I/O. Runtime support for sockets does not grant that capability.

Migration and deletion

  1. Specify executable schemas for wheeler.package.yaml, wheeler.workspace.yaml, wheeler.package.lock.yaml, .wpk, and build plans.
  2. Add stage-0 readers and canonical writers with malformed-input and reproducibility suites.
  3. Implement workspace module resolution and replace hard-coded Gradle project knowledge.
  4. Implement locked local/path dependencies, then vendored and registry dependencies.
  5. Add the XDG data repository, content-keyed artifact cache, quarantine state, and explicit cache maintenance.
  6. Implement check, build, test, run, doc, package, and clean over the Wheeler compiler and native runtime.
  7. Implement the Wheeler package manager and compare every plan, lockfile, archive, diagnostic, and failure with stage 0.
  8. Bootstrap the complete repository from a vendored recovery workspace with no network.
  9. Switch ordinary CI and release jobs to native wheeler.
  10. Delete Gradle files, wrappers, Java package tasks, host-language manifest readers, and duplicate shell orchestration.
  11. Add remote registry publication only after local, vendored, cached, and recovery builds are stable.

Progress

Testing and acceptance

Alternatives

Keep Gradle

Rejected. It preserves Java in Wheeler's bootstrap and makes host plugins an unrestricted semantic dependency.

Use Cargo directly

Rejected. Cargo is a strong model for user experience, but Wheeler packages need Wheeler modules, effects, reversibility, quantum capabilities, .wbc, proof metadata, and native recovery identities. Depending on Cargo would also replace a Java bootstrap dependency with Rust's.

Accept arbitrary YAML

Rejected. Familiar indentation is useful, but implicit typing, aliases, merge keys, tags, multiple documents, and duplicate keys are outside the package format. Wheeler accepts one closed YAML 1.2 subset that standard readers can inspect. The recovery seed does not need a general YAML implementation.

Use arbitrary Wheeler build scripts

Rejected. General scripts make dependency discovery effectful, prevent static capability review, and reproduce unrestricted install-hook failures. Declarative plans plus capability-scoped Wheeler tools are sufficient.

Make every dependency a source checkout

Rejected. It lacks immutable archive identity, offline verification, namespace ownership, and bounded extraction semantics.

Put provider configuration in package features

Rejected. Credentials, queue selection, calibration, budgets, and hardware availability are deployment policy. Packages declare semantic target requirements only.

Open questions

Integration with reversible concurrency

Task-profile compatibility

Public API identity includes task, shared, and blocking effects, transfer modes, required task extension, memory model, and public witness and resource contracts.

Build nodes may run concurrently as engine work. Output and diagnostics still reduce in canonical graph order. Build workers are not Wheeler Tasks unless the tool executes a WIP-0039 artifact.

Worker count and native scheduling do not enter package identity. Required physical concurrency remains explicit deployment policy.

References