Concepts

The project root

The project root is the directory with the zenith.toml. zenith finds it by searching upward from the working directory, or takes it from --config or --root. Every path in the configuration, in the output and in the baseline is a POSIX path relative to the root, with / separators on every platform.

Nodes

zenith models the codebase as a tree of nodes, with the external packages and builtins that it imports:

its subtree zenith.toml web web/src web/src/app web/src/app/page.tsx web/src/lib web/src/lib/images.ts web/src/lib/theme.css npm:react-dom npm:sharp node:fs project root outside the project "react-dom/client" "fs/promises" "sharp" directory file external package builtin
A node's id is its path from the project root. Packages and builtins are named without their subpath.
NodeExample
Directoryweb/src/domainsEvery ancestor directory of a file. A directory stands for its whole subtree, and is also what an import of a Go package refers to.
Fileweb/src/domains/ai/api/client.tsEvery source file, and every file inside the root that an import resolves to, such as .css or .json.
External packagenpm:react, gomod:github.com/pkg/errorsA package from outside the project, with its language's scheme.
Builtinnode:fs, go:net/httpA module of a runtime or standard library, with its language's scheme.

Each language says how it names its external packages and builtins. A file such as an index.ts is an ordinary file, with no special role for its directory.

Workspace packages

Each language finds the packages of the project: the workspace packages of a JavaScript monorepo, the modules of a Go project, and the crates of a Cargo workspace. Each has a name and a directory, and names and directories often differ, so contracts can name packages with pkg: selectors. The languages page says where each language finds them.

web apps/web @acme/ui packages/ui @acme/core packages/core pkg:@acme/* package.json { "workspaces": [ "apps/*", "packages/*" ] } or pnpm-workspace.yaml name directory
A pkg: selector matches packages by name, and picks their directories.

Imports

An import is an edge from a file to a file, directory, package or builtin. Each import statement is recorded with its kind:

web/src/lib/db.ts value web/src/types.ts type web/src/app/chart.tsx dynamic npm:sharp require skipped web/src/app/page.tsx import { db } from "@/lib/db"; import type { User } from "@/types"; const chart = import("./chart"); const sharp = require("sharp"); const page = import(path); not a literal, so no edge
Each statement is an edge with its kind. A specifier that isn't a literal is skipped.
Kind
valuean ordinary import, which every language has
typean import that is erased at runtime, such as TypeScript's import type
dynamican import made while the code runs, such as JavaScript's import("s")
requirea CommonJS require("s")

The languages page lists the statements that make each kind. Contracts can leave out imports by kind with exclude_kinds: type-only imports are erased at runtime, so they are often excluded.

Re-exports

In JavaScript and TypeScript, a pass-through module only re-exports other modules, such as an index.ts of export * from "./a" and export { b } from "./b" lines, with no code of its own. JavaScript calls it a barrel. Through it, the import graph has a chain to every module it re-exports, so import { format } from "./lib" reaches whatever else lib/index.ts re-exports, even server code. Bundlers that know the module has no side effects leave the unused modules out.

A contract with reexports = "transparent" sees an import of names from a pass-through module as imports of the modules that define them, following re-exports and export * through nested pass-through modules. An import that takes the whole module, such as import * as lib from "./lib", still imports all of it, and so does an import of a name that can't be followed, such as one that may come from an external package through export *. Importing only a name's type is a type import.

[contracts.client-safety]
type = "forbidden"
from = [{ path = "web/src", directive = "use client" }]
to = ["node:*", "npm:sharp"]
reexports = "transparent"
exclude_kinds = ["type"]

encapsulation contracts always see re-exports as they are, since importing a module through its entry point, which often only re-exports, is what they allow.

Units

Contracts reason about units: labelled sets of nodes, which selectors pick. A matched directory is one unit, with its whole subtree as members; a matched file, package or builtin is a unit of one.

web/src/features/* web/src/features/checkout page.tsx form.tsx web/src/features/catalog api.ts list.tsx web/src/features/shared button.tsx hooks/ npm:sharp npm:sharp unit unit unit a unit of one
Each directory that web/src/features/* matches is a unit, with everything under it.

Two units overlap if they share a member, and contracts skip pairs of overlapping units, so from = ["a"] and to = ["a/*"] checks nothing between a and itself.

Directives

A directive is a string literal at the very top of a file, before any other code, such as "use client". In React Server Components, "use client" makes a file the entry point of a client bundle, so everything it imports ships to the browser. zenith records every directive as written, and a directive selector can pick the files that have one, to draw a boundary that the directory layout doesn't show.

server client bundle: everything here ships to the browser app/page.tsx "use client" components/upload.tsx "use client" components/chart.tsx lib/images.ts npm:sharp npm:recharts no directive a unit of { path = "web/src/**", directive = "use client" }
The client components start the client bundle, wherever they are. A directive selector makes each of them a unit.