Skip to content

12. Packages

A package is a directory containing a package.archspace manifest and any number of .arch files under it. The manifest names the package, declares its dependencies on other packages, points to a widgets script, and selects which types are imported into scope.

package: acme.shop
version: "1.4.0"
dependencies {
acme.shared: "../shared"
acme.payments: "../packages/payments"
}
use service, database, gateway, kafka from arch.backend
use Database, Cache from acme.shared

That’s a full-shaped manifest. This chapter covers every field, every diagnostic, and the resolution rules.

Rule. Default to a single root space that is also the package — keep it a monorepo. For the overwhelming majority of projects, one root package is the right answer. You reach for nested spaces only when ownership genuinely diverges across enterprise org divisions (see Spaces). Spaces draw ownership/visibility lines, not technical layers.

A directory without a manifest is an anonymous package: the loader walks every .arch file under it and resolves them together with no name, no dependencies, no exports.

A standalone .arch file is a first-class, encouraged way to start — perfect for a quick draft. The one constraint: a single file can depend only on locally defined types and standard-library types, never on custom types from another package (there’s no package-linking syntax for a lone file). That’s plenty — the stdlib is vast — and you promote the draft into a managed package when it earns one.

package: (required once, at the package root)

Section titled “package: (required once, at the package root)”

The package identifier, and an opaque resolution boundary: dependents see only its exported types. Dotted form is conventional:

package: acme.shop
  • At least one segment.
  • Lowercase, dot-separated by convention; not enforced.
  • The arch.* prefix is reserved for the bundled stdlib. A user package outside the toolchain’s stdlib/ directory claiming arch.<anything> emits RESERVED_PACKAGE_NAMESPACE.

Duplicate package: lines are an error; the first occurrence wins so tooling has something stable to work with. (name: is a different field — it declares a space within a package; see Spaces. A manifest carries one or the other, not both.)

Naming conventions for the dotted segments:

  • Publishing types for others to consume? Prefix with your reverse company domain (com.acme.payments) so the name is a globally-unique address.
  • Just your own repo? A bare company or project name is fine — acme.payments, or simply payments. No domain prefix needed.
  • Spaces continue the package’s dotted path as sub-packages of the root (root acme → space acme.retailbanking).
  • Stdlib packages keep their roots: arch.cloud.aws, arch.extras, …
version: "1.4.0"

Free-form string. Currently informational only — the loader doesn’t parse, compare, or constrain versions. Reserve the field for forward compatibility; future tooling will use semver-style ranges.

Path (relative to the manifest dir) to a JS/TS module that registers custom-element widgets via customElements.define():

widgets: "./widgets.js"

The viewer dynamic-imports this script at boot and re-imports it on file changes (after a full page reload — customElements.define is one-shot per tag name).

The loader verifies the file exists at load time. A typo emits WIDGETS_FILE_NOT_FOUND immediately. The widget pipeline is the subject of Chapter 20.

repo: / commit: (optional, the evidence pin)

Section titled “repo: / commit: (optional, the evidence pin)”
repo: "https://github.com/acme/shop"
commit: "a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0"

The code repository this package’s sources: evidence bindings are checked against, and the revision they are true of. commit: is required whenever repo: is set — an unpinned path attests that a file existed once, not that the model is true of a commit. Scoping the pin to the package, rather than repeating a repository beside every binding, is what keeps a bare path unambiguous however many dependencies the package has.

Both are informational to the loader; they are read by the evidence checker. Chapter 37 covers the bindings themselves.

dependencies { ... } (optional, at most one block)

Section titled “dependencies { ... } (optional, at most one block)”

Map from dependency package name to filesystem path:

dependencies {
acme.shared: "../shared"
acme.payments: "../../packages/payments"
}

For each entry, the loader:

  1. Resolves the path against the manifest’s directory.
  2. Loads the dependency package recursively (its own dependencies load too).
  3. Verifies the loaded package’s name: matches the depender’s declared name. Mismatch emits DEP_NAME_MISMATCH.

Diamond dependencies (A→B, A→C, both →D) load D once and share the same instance — the loader caches by absolute path.

Cycles (A→B, B→A) emit DEP_CYCLE on every package along the cycle.

arch.* packages don’t need an entry. The loader auto-resolves them against the toolchain’s bundled stdlib (configurable via ARCHLANG_STDLIB).

Imports types from a dependency or stdlib package:

// Import a single type
use database from arch.backend
// Several
use service, frontend from arch.backend
// Wildcard — every exported type (discouraged; never on the stdlib — see the rule below)
use * from acme.internal
// Rename
use database as managed_db from arch.backend
// Re-export so consumers of THIS package see it too
export use payments_provider from acme.payments
FormEffect
use X from pImports X directly. X must be marked export in p — otherwise USE_TYPE_NOT_EXPORTED.
use * from pImports every type marked export in p.
use X as Y from pImports X under the local name Y.
export use X from pRe-exports X so wildcard importers of this package also receive X.

Rule. Prefer explicit use; treat a package as a palette. Pick the types you actually need by name — use postgres, kafka from store.backend, not use * from store.backend. A wildcard drags in a flood of unused types and couples you to the package’s whole surface. use * is reasonable only for a small, company-internal library you wrote and want all of; never wildcard the standard library. And declare the palette at the space level (the package.archspace manifest) so it’s space-wide and discoverable — bias to one shared use list over re-importing the same types file by file.

  • A use declared in the package-root package.archspace is package-scoped: every .arch file in this package can reference the imported names.
  • A use declared in a space manifest (a nested package.archspace with name:) is space-scoped: only files within that space’s subtree see the names. Vocabulary stays local — a postgres type used in one space need not be named in another.
  • A use declared inside a .arch file is file-scoped: only that file can reference them. References from a sibling file emit TYPE_NOT_VISIBLE_IN_FILE. (This is exactly what makes single-file drafts work.)

Inner scopes shadow outer; nearest-first. Bias toward the space level: a one-off local need can import close to use, but a single space-wide use list is self-documenting — someone opening the manifest sees the whole palette at a glance.

The same name imported from the same source package across multiple files is fine — scopes accumulate. The same name from different source packages produces USE_NAME_COLLISION; rename one with as.

Anonymous packages skip the export check: every type is reachable. Once a package gains a manifest, types must be marked export to surface to dependents.

CodeWhen
MANIFEST_PARSE_ERRORMalformed manifest syntax
RESERVED_PACKAGE_NAMESPACEA non-stdlib package claims the arch.* prefix
DEP_LOAD_FAILEDA declared dependency path doesn’t exist or fails to parse
DEP_NAME_MISMATCHThe loaded package’s name: differs from the depender’s expectation
DEP_CYCLEA cycle in the package dependency graph
STDLIB_NOT_FOUNDAn arch.* import couldn’t be resolved against the configured stdlib
USE_PACKAGE_NOT_FOUNDuse ... from <pkg> references a package neither in dependencies nor arch.*
USE_TYPE_NOT_FOUNDThe named type doesn’t exist in the source package
USE_TYPE_NOT_EXPORTEDThe type exists but isn’t marked export
USE_NAME_COLLISIONThe same local name imported from two different source packages
WIDGETS_FILE_NOT_FOUNDThe widgets: path doesn’t resolve to an existing file

Minimal scratchpad:

package: scratch

A scratch project that doesn’t depend on anything. Every .arch file in the directory is loaded and gets to import from each other; no arch.* types are visible because use isn’t declared.

Standard project using stdlib:

package: acme.shop
version: "0.4.0"
use service, database, gateway from arch.backend
use rest_create, rest_read, rest_update, kafka, grpc_unary from arch.backend

The most common shape — a curated palette of the bundled module types (service, database, …) and wire-level interface types (rest_create, rest_read, kafka, grpc_unary, …), picked by name rather than wildcarded.

Multi-package monorepo:

package: acme.app
version: "1.0.0"
widgets: "./widgets.js"
dependencies {
acme.shared: "../shared"
acme.payments: "../packages/payments"
}
use service, database, gateway, kafka from arch.backend
use Database, Cache from acme.shared
export use payments_provider from acme.payments

Declares deps, project-wide imports, plus a re-export so anything depending on acme.app also picks up payments_provider.

Library package:

package: acme.shared
version: "2.1.0"

The library defines types in its .arch files and marks the public ones with export. Consumers use them by name; non-exported types remain internal.

A space is a visibility boundary inside a package, declared by a nested package.archspace carrying name: instead of package:. Unlike a package boundary (opaque, type-only), a space is transparent — one model, cross-space coupling still visible, gated only by export.

akme/package.archspace
package: akme.bank
akme/corporate/loans/package.archspace
name: Corporate.Loans

The decision is ownership, not technical layering:

  • Default: one root space = the package = a monorepo. Small and mid-size systems pay no boundary cost — keep everything in the single root.
  • Nest spaces only when ownership genuinely diverges: a different set of people owns a very large, organizationally closed domain — enterprise divisions backed by separate domain leaders that barely collaborate. That org-level separation is what spaces express.
  • Don’t reach for spaces to model decomposition or layers; that’s nesting and aspects’ job, not the visibility boundary’s.

Keep types close to their instances — a type lives in the same namespace as, and domainly close to, the modules that use it. The escape hatch for a large org is a dedicated shared-types package nested inside a package: opaque, exporting only what it chooses, and useable from anywhere. That buys one owned home for shared vocabulary instead of scattering or duplicating definitions.

my-project/
├── package.archspace # package: my.project (package root)
├── widgets.js # custom-element registrations (optional)
├── orders.arch # the orders domain: modules, processes, views
├── business-primitives.arch # custom business types used across modules
└── packages/
└── shared/
├── package.archspace # nested package — its OWN opaque unit
└── lib.arch

A nested package.archspace with package: is a hard, opaque boundary: the parent’s file walk stops at its directory, and it resolves as its own unit only when referenced via dependencies or auto-loaded as arch.*. A nested manifest with name: is a transparent space — the walk continues through it, modules stay visible across the boundary as resolved edges, gated by export.

  • Every project has a package.archspace manifest at its root, naming the package with package:.
  • Fields: package: (required), version:, widgets:, dependencies { }, use … from ….
  • Default to a single root package/monorepo; nest name: spaces only for enterprise org divisions where ownership diverges.
  • Prefer explicit use over use * — pick types by name, declared space-wide in the manifest; never wildcard the stdlib.
  • arch.* is reserved for stdlib and auto-resolves; user dependencies need a path.
  • use in the package manifest is package-scoped; in a space manifest, space-scoped; in a .arch file, file-scoped (which is what makes single-file drafts work).
  • Diamond deps share one instance; cycles emit DEP_CYCLE.

Chapter 13: Stable IDs → — identity that survives renames, and where IDs do and don’t apply.