# Packages & Modules — zero.json, Imports, and Dependency Resolution

> How multi-file Zero projects are structured: the zero.json manifest schema (package, targets, dependencies), how use-imports resolve from src/ (including mod.0 fallback), local path dependencies requiring zero.json, and the deterministic lock written to .zero/package-locks/. Key failure modes: IMP001 (unknown import), PKG001 (missing manifest), IMP002/PKG002 (cycles).

- Repository: vercel-labs/zerolang
- GitHub: https://github.com/vercel-labs/zerolang
- Human wiki: https://grok-wiki.com/public/wiki/vercel-labs-zerolang-9ab46b2a38e0
- Complete Markdown: https://grok-wiki.com/public/wiki/vercel-labs-zerolang-9ab46b2a38e0/llms-full.txt

## Source Files

- `skill-data/zero-packages.md`
- `examples/std-platform.0`
- `conformance/packages`
- `native/zero-c/src/call_resolve.c`
- `scripts/provenance-guardrails.mts`

---

<details>
<summary>Relevant source files</summary>
The following files were used as context for generating this wiki page:

- [skill-data/zero-packages.md](skill-data/zero-packages.md)
- [docs/articles/package-manifest.md](docs/articles/package-manifest.md)
- [docs/articles/diagnostics.md](docs/articles/diagnostics.md)
- [native/zero-c/src/fs.c](native/zero-c/src/fs.c)
- [native/zero-c/src/main.c](native/zero-c/src/main.c)
- [conformance/packages/dep-app/zero.json](conformance/packages/dep-app/zero.json)
- [conformance/packages/cycle-a/zero.json](conformance/packages/cycle-a/zero.json)
- [conformance/packages/conflict-app/zero.json](conformance/packages/conflict-app/zero.json)
- [conformance/packages/target-incompatible-app/zero.json](conformance/packages/target-incompatible-app/zero.json)
- [conformance/packages/test-app/src/main.0](conformance/packages/test-app/src/main.0)
- [examples/std-platform.0](examples/std-platform.0)
- [scripts/provenance-guardrails.mts](scripts/provenance-guardrails.mts)
</details>

# Packages & Modules — zero.json, Imports, and Dependency Resolution

Zero organizes multi-file programs into _packages_: directories rooted at a `zero.json` manifest. The manifest declares the package identity, executable targets, and dependency references. The compiler resolves all intra-package module imports from the `src/` tree, resolves local path dependencies recursively, and writes a deterministic dependency fingerprint into `.zero/package-locks/` before any compilation step. Understanding this flow is essential for authoring, debugging, and tooling Zero projects—the same model governs both standalone programs and library packages shared across a repository.

This page covers the full lifecycle: the `zero.json` schema, how `use`-imports resolve to `src/` files (including the `mod.0` directory convention), how local path dependencies are validated and linked, what the lock file contains and why, and the complete set of diagnostic codes that fire when any of these invariants are violated.

---

## The zero.json Manifest

Every package is identified by a `zero.json` file at its root. Passing either the package directory or the manifest path itself to any `zero` command is equivalent—the compiler detects both forms.

```json
{
  "package": { "name": "hello", "version": "0.1.0", "license": "MIT" },
  "targets": { "cli": { "kind": "exe", "main": "src/main.0" } },
  "dependencies": {
    "local-tools": { "path": "../local-tools", "version": "0.1.0" },
    "registry-tools": "1.2.3"
  },
  "profiles": {
    "dev":     { "inherits": "dev" },
    "release": { "inherits": "release" }
  }
}
```
Sources: [docs/articles/package-manifest.md:1-19]()

### Schema Fields

| Field | Required | Description |
|---|---|---|
| `package.name` | Yes | Package identity string used in dependency graphs and lock files |
| `package.version` | Yes | SemVer string; must match what dependents declare |
| `package.license` | No | Optional SPDX license identifier |
| `targets.<name>.kind` | Yes | Only `"exe"` is supported in the native bootstrap compiler |
| `targets.<name>.main` | Yes | Entry-point file path relative to the package root (e.g., `"src/main.0"`) |
| `dependencies.<alias>` | No | Object with `path` + `version` for local; bare version string for registry metadata |
| `profiles` | No | Named build profiles (`dev`, `release`) that inherit built-in defaults |

The `targets` field can be named anything (by convention `"cli"` for executables). The `kind` field under a target controls what the compiler emits. The `main` field is the file the resolver uses as the entry point from which all `use`-imports are discovered.

Sources: [native/zero-c/src/fs.c:1343-1358](), [docs/articles/package-manifest.md:1-19]()

---

## Module Imports: use-import Resolution

Within a package, modules are imported using the `use` keyword. The compiler scans every source file for `use` lines, translates the module name into a file path relative to `src/`, and recursively loads the referenced module.

### Resolution Algorithm

Given `use config.parser` in any source file, the resolver performs these two steps in order:

1. **Direct file**: translate dots to slashes and append `.0` → look for `src/config/parser.0`
2. **Directory module (mod.0 fallback)**: strip `.0`, treat as a directory name → look for `src/config/parser/mod.0`

If neither path exists, the resolver emits `IMP001`.

```
use helpers        →  src/helpers.0            (or src/helpers/mod.0)
use config.parser  →  src/config/parser.0      (or src/config/parser/mod.0)
```

Sources: [native/zero-c/src/fs.c:428-451](), [skill-data/zero-packages.md:43-45]()

The exact C implementation that performs this lookup:

```c
// native/zero-c/src/fs.c:428-451
static char *module_path_to_source(const char *src_root, const char *module_name) {
  // 1. Try src/<module/path>.0
  ...
  if (file_exists(file_path)) return file_path;
  // 2. Try src/<module/path>/mod.0
  char *mod_path = join_path(dir_path, "mod.0");
  if (file_exists(mod_path)) return mod_path;
  return NULL;
}
```

### Standard Library Imports

`use` lines whose module name begins with `std.` are treated specially: they are skipped by the local file resolver and handed to the standard library built into the compiler. They never generate `IMP001` even if no matching file exists in `src/`.

```c
// native/zero-c/src/fs.c:703-705
if (strncmp(module_name, "std.", 4) == 0) {
    free(module_name);
} else {
    // resolve local module ...
}
```

Sources: [native/zero-c/src/fs.c:700-715]()

### Import Syntax

The current compiler recognizes two forms, but only `use` is idiomatic:

```zero
use helpers          // idiomatic
import helpers       // legacy form, still parsed but not recommended
```

Sources: [native/zero-c/src/fs.c:685-701]()

### Module Name-to-Symbol Mapping

The reverse mapping (file path → module name visible to the program) replaces path separators with dots and strips the `.0` extension (and trailing `/mod` for directory modules). So `src/config/parser.0` presents its public symbols under `config.parser`.

Sources: [native/zero-c/src/fs.c:396-411]()

---

## Dependency Declaration and Resolution

### Local Path Dependencies

Local dependencies must be declared with both `path` and `version`. The `path` must point to a directory that contains a valid `zero.json`.

```json
{
  "dependencies": {
    "dep-lib": { "path": "../dep-lib", "version": "0.1.0" }
  }
}
```
Sources: [conformance/packages/dep-app/zero.json:1-8]()

The resolver performs a depth-first traversal of all dependencies. For each local path entry:

1. It resolves the path relative to the current manifest and checks whether `zero.json` exists there (`PKG001` if not).
2. It checks whether this manifest path is already on the resolution stack (`PKG002` if a cycle is detected).
3. It parses the target manifest and compares the resolved `package.version` against the requested version (`PKG003` if they differ).
4. It recurses into that dependency's own dependencies.

```c
// native/zero-c/src/fs.c:1249-1303
static bool resolve_manifest_dependencies(...) {
  for (each dep in manifest->dependencies) {
    if (!dep->path) { push registry-reference; continue; }
    if (!file_exists(dep_manifest_path)) → PKG001;
    if (dependency_stack_contains(dep_manifest_path)) → PKG002;
    if (version mismatch) → PKG003;
    if (name already seen with different version) → PKG003;
    recurse into dep...
  }
}
```

Sources: [native/zero-c/src/fs.c:1249-1305]()

### Registry Metadata Dependencies

A bare version string dependency (e.g., `"registry-tools": "1.2.3"`) is recorded as `registry-reference` metadata without fetching or resolving any code. The resolver does not perform remote fetches.

Sources: [native/zero-c/src/fs.c:1252-1254](), [skill-data/zero-packages.md:63-70]()

---

## The Lock File: .zero/package-locks/

After a successful dependency resolution, the compiler writes a deterministic lock file under `.zero/package-locks/<hash>.lock.json`. The filename is the 64-bit FNV-1a hash of the resolved dependency graph, formatted as a 16-character hex string.

### Lock File Format

```json
{
  "schemaVersion": 1,
  "format": "zero-lock-v1",
  "package": { "name": "dep-app", "version": "0.1.0" },
  "dependencyGraphHash": "a1b2c3d4e5f60001",
  "dependencies": [
    {
      "name": "dep-lib",
      "version": "0.1.0",
      "resolvedName": "dep-lib",
      "resolvedVersion": "0.1.0",
      "status": "path-resolved",
      "fingerprint": "0123456789abcdef"
    }
  ]
}
```

Sources: [native/zero-c/src/fs.c:1209-1247]()

The `status` field records how each dependency was resolved:

| Status | Meaning |
|---|---|
| `path-resolved` | Local directory confirmed to contain `zero.json` |
| `registry-reference` | Bare version string; no code fetched |

### Lock File as a Cache Key Input

The lock file hash feeds directly into the compiler cache key. The full set of cache-key inputs is:

| Input | Role |
|---|---|
| Compiler version (`ZERO_VERSION`) | Invalidate on toolchain upgrades |
| Target name | Separate caches per build target |
| Package version | From `package.version` in `zero.json` |
| `dependencyGraphHash` | FNV-1a hash of all resolved dep fingerprints |
| `manifestHash` | FNV-1a hash of the `zero.json` text |
| `lockfileHash` | FNV-1a hash of the written lock file |

```c
// native/zero-c/src/main.c:2144-2156
zbuf_append(buf, "{\"cacheKeyInputs\":{\"compilerVersion\":");
// ... target, packageVersion, dependencyGraphHash, manifestHash, lockfileHash
zbuf_append(buf, ",\"invalidationReasons\":[\"source changed\",\"manifest changed\",
  \"dependency graph changed\",\"target changed\",\"profile changed\",
  \"compiler version changed\"]");
```

Sources: [native/zero-c/src/main.c:2144-2157]()

---

## Resolution Flow Diagram

```text
zero check/build <package-dir>
        │
        ▼
  Read zero.json
  (manifest_path, main_path)
        │
        ├──► Resolve dependencies (DFS)
        │         │
        │         ├── local path dep → verify zero.json exists (PKG001)
        │         ├── cycle check on stack (PKG002)
        │         ├── version match check (PKG003)
        │         └── recurse into transitive deps
        │
        ├──► Write .zero/package-locks/<graph-hash>.lock.json
        │
        └──► Resolve use-imports (src/ tree)
                  │
                  ├── use std.* → built-in, skip file lookup
                  ├── use <name> → src/<name>.0
                  │                  └── fallback: src/<name>/mod.0
                  │                  └── missing → IMP001
                  └── cycle in import graph → IMP002
```

---

## Diagnostic Codes Reference

All package and import resolution failures use stable diagnostic codes. Every package-level failure uses `fixSafety: "requires-human-review"` because the correct repair may change package topology.

### Import Diagnostics

| Code | Trigger | Repair |
|---|---|---|
| `IMP001` | `use <name>` resolves to neither `src/<name>.0` nor `src/<name>/mod.0` | Create the module file, fix its path, or remove the `use` |
| `IMP002` | A chain of `use`-imports forms a cycle within the package | Break the cycle by extracting shared declarations into a third module |
| `IMP003` | Two imported modules both export the same public name | Rename one export or choose only one of the two modules |

Sources: [native/zero-c/src/fs.c:706-733](), [docs/articles/diagnostics.md:107-109]()

### Package Diagnostics

| Code | Trigger | Repair |
|---|---|---|
| `PKG001` | A local dependency `path` does not point to a directory with `zero.json` | Fix the path or create the dependency package |
| `PKG002` | Package A depends on B, which depends back on A (or any cycle) | Move shared code into a separate third package |
| `PKG003` | The same package name is resolved to two different versions in the graph | Choose one version for the whole graph |
| `PKG004` | The dependency's `targets` list in the manifest does not include the selected build target | Select a compatible target or restrict the dependency behind target-specific metadata |

Sources: [native/zero-c/src/fs.c:1257-1294](), [native/zero-c/src/main.c:8911-8923](), [docs/articles/diagnostics.md:258-266]()

### PKG004 in Detail

When a dependency declaration includes a `targets` array (see the conformance fixture below), the compiler validates that the current build target is listed before proceeding:

```json
// conformance/packages/target-incompatible-app/zero.json
{
  "dependencies": {
    "target-webbits": {
      "path": "../target-webbits",
      "version": "0.1.0",
      "targets": ["win32-x64.exe"]
    }
  }
}
```

If you build this for `darwin-arm64`, the compiler fires `PKG004` because `darwin-arm64` is not in `["win32-x64.exe"]`. An empty or absent `targets` array means the dependency is target-neutral.

Sources: [conformance/packages/target-incompatible-app/zero.json:1-10](), [native/zero-c/src/main.c:2091-2094]()

---

## Introspection Commands

```sh
# Inspect the resolved module graph, import edges, lockfile path, and cache key inputs
zero graph --json <package>

# Inspect public API symbols for each module
zero doc --json <package>

# Run type-checking with full JSON diagnostics
zero check --json <package>

# Get a human-readable explanation of any diagnostic code
zero explain PKG001
zero explain IMP002
```

The `graph` command reports `package.dependencies`, `package.lockfile`, `package.resolver`, and `packageCache.cacheKeyInputs`—the last item exposes the inputs that, when changed, trigger a rebuild.

Sources: [skill-data/zero-packages.md:74-84](), [docs/articles/package-manifest.md:31-50]()

---

## Common Repairs Quick Reference

| Symptom | Code | Fix |
|---|---|---|
| `use helpers` fails to resolve | `IMP001` | Create `src/helpers.0` or `src/helpers/mod.0` |
| Two modules import each other | `IMP002` | Extract shared declarations to a new `src/shared.0` |
| `../missing-lib` has no `zero.json` | `PKG001` | Create the package or correct the `path` |
| cycle-a depends on cycle-b and vice versa | `PKG002` | Extract the common interface into a third package |
| `shared-v1` (0.1.0) and `shared-v2` (0.2.0) both named `shared` | `PKG003` | Alias to different names or pin to one version |
| Building for `darwin-arm64` but dep only lists `win32-x64.exe` | `PKG004` | Remove the `targets` restriction or choose a compatible build target |

The deterministic lock file written to `.zero/package-locks/` means that if you see a cache miss after changing a dependency or its version, the old lock file is simply superseded—no manual cleanup is required. The compiler writes a new one keyed to the new graph hash on every successful resolve.

Sources: [native/zero-c/src/fs.c:1209-1247](), [docs/articles/package-manifest.md:38-47]()
