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.shopversion: "1.4.0"
dependencies { acme.shared: "../shared" acme.payments: "../packages/payments"}
use service, database, gateway, kafka from arch.backenduse Database, Cache from acme.sharedThat’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.
Anonymous packages and single-file drafts
Section titled “Anonymous packages and single-file drafts”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.
Fields
Section titled “Fields”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’sstdlib/directory claimingarch.<anything>emitsRESERVED_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 simplypayments. No domain prefix needed. - Spaces continue the package’s dotted path as sub-packages of the root (root
acme→ spaceacme.retailbanking). - Stdlib packages keep their roots:
arch.cloud.aws,arch.extras, …
version: (optional, exactly once)
Section titled “version: (optional, exactly once)”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.
widgets: (optional, exactly once)
Section titled “widgets: (optional, exactly once)”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:
- Resolves the path against the manifest’s directory.
- Loads the dependency package recursively (its own dependencies load too).
- Verifies the loaded package’s
name:matches the depender’s declared name. Mismatch emitsDEP_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).
use … from <pkg> (zero or more)
Section titled “use … from <pkg> (zero or more)”Imports types from a dependency or stdlib package:
// Import a single typeuse database from arch.backend
// Severaluse service, frontend from arch.backend
// Wildcard — every exported type (discouraged; never on the stdlib — see the rule below)use * from acme.internal
// Renameuse database as managed_db from arch.backend
// Re-export so consumers of THIS package see it tooexport use payments_provider from acme.payments| Form | Effect |
|---|---|
use X from p | Imports X directly. X must be marked export in p — otherwise USE_TYPE_NOT_EXPORTED. |
use * from p | Imports every type marked export in p. |
use X as Y from p | Imports X under the local name Y. |
export use X from p | Re-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, notuse * 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 (thepackage.archspacemanifest) so it’s space-wide and discoverable — bias to one shareduselist over re-importing the same types file by file.
Visibility scope
Section titled “Visibility scope”- A
usedeclared in the package-rootpackage.archspaceis package-scoped: every.archfile in this package can reference the imported names. - A
usedeclared in a space manifest (a nestedpackage.archspacewithname:) is space-scoped: only files within that space’s subtree see the names. Vocabulary stays local — apostgrestypeused in one space need not be named in another. - A
usedeclared inside a.archfile is file-scoped: only that file can reference them. References from a sibling file emitTYPE_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.
Diagnostic codes
Section titled “Diagnostic codes”| Code | When |
|---|---|
MANIFEST_PARSE_ERROR | Malformed manifest syntax |
RESERVED_PACKAGE_NAMESPACE | A non-stdlib package claims the arch.* prefix |
DEP_LOAD_FAILED | A declared dependency path doesn’t exist or fails to parse |
DEP_NAME_MISMATCH | The loaded package’s name: differs from the depender’s expectation |
DEP_CYCLE | A cycle in the package dependency graph |
STDLIB_NOT_FOUND | An arch.* import couldn’t be resolved against the configured stdlib |
USE_PACKAGE_NOT_FOUND | use ... from <pkg> references a package neither in dependencies nor arch.* |
USE_TYPE_NOT_FOUND | The named type doesn’t exist in the source package |
USE_TYPE_NOT_EXPORTED | The type exists but isn’t marked export |
USE_NAME_COLLISION | The same local name imported from two different source packages |
WIDGETS_FILE_NOT_FOUND | The widgets: path doesn’t resolve to an existing file |
Worked examples
Section titled “Worked examples”Minimal scratchpad:
package: scratchA 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.shopversion: "0.4.0"
use service, database, gateway from arch.backenduse rest_create, rest_read, rest_update, kafka, grpc_unary from arch.backendThe 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.appversion: "1.0.0"widgets: "./widgets.js"
dependencies { acme.shared: "../shared" acme.payments: "../packages/payments"}
use service, database, gateway, kafka from arch.backenduse Database, Cache from acme.sharedexport use payments_provider from acme.paymentsDeclares deps, project-wide imports, plus a re-export so anything depending on acme.app also picks up payments_provider.
Library package:
package: acme.sharedversion: "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.
Spaces within a package
Section titled “Spaces within a package”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.
package: akme.bankname: Corporate.LoansThe 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.
File layout
Section titled “File layout”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.archA 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.
Summary
Section titled “Summary”- Every project has a
package.archspacemanifest at its root, naming the package withpackage:. - 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
useoveruse *— 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.usein the package manifest is package-scoped; in a space manifest, space-scoped; in a.archfile, file-scoped (which is what makes single-file drafts work).- Diamond deps share one instance; cycles emit
DEP_CYCLE.
What’s next
Section titled “What’s next”Chapter 13: Stable IDs → — identity that survives renames, and where IDs do and don’t apply.