# Modules

Once graph modules are Starlark files that export graph primitives. The primary
exported primitive is a target kind: a schema, an optional resolver that expands
locked dependency metadata into targets, and an optional implementation that
turns one target into cacheable actions. Built-in and project target kinds use
the same public contract, so a project can add a target kind without changing
Once itself.

Built-in analysis loads the target-kind families needed by the graph together
with their helper dependencies. Shared, ecosystem-neutral utilities remain
available across families; toolchain-specific helpers do not need to be loaded
for unrelated targets. This keeps analysis work proportional to the graph
without changing the schemas available through discovery.

Built-in Apple target kinds use Xcode tool discovery on macOS and the registered
xtool Darwin Swift SDK on Linux. The existing `xcode_developer_dir` attribute
pins Xcode's `Developer` directory on macOS or a Darwin SDK bundle's `Developer`
directory on Linux. Linux iOS device targets default to ARM64, independently of
host architecture; `archs` still overrides library architectures. Compiler,
SDK, resource-tool, and environment identities partition Linux action caches.
Signing actions invoke `rcodesign` with ad-hoc signing and no timestamp service.
Providers, action declarations, and resource budgeting are unchanged. The
Linux starter is discoverable through `once query example apple_application
apple-application-linux`, and the same example is available through MCP.
See [Build iOS Applications on Linux](/guide/graph/apple/linux) for setup and
unsupported macOS runtime and resolver workflows.

## Loading Project Modules

Project modules are listed from the root manifest:

```toml
[modules]
paths = ["modules/*.star"]
```

Each matched file exports one or more public target kind symbols. Public
symbols are module globals that do not start with `_`. The exported
symbol name becomes the target kind unless the target kind explicitly sets
`kind`.

```python
def _copy_impl(ctx):
    out = declare_output("copied.txt")
    srcs = glob(ctx["srcs"])
    copy_path(
        srcs[0],
        out,
        inputs = srcs,
        identifier = ctx["label"]["id"] + ":copy",
    )
    return {
        "label_id": ctx["label"]["id"],
        "copied_file": out,
    }

copy_generated = target_kind(
    docs = "Copy one declared source file into the target output directory.",
    attrs = [],
    providers = ["copied_file"],
    capabilities = [capability("build", ["default"])],
    impl = _copy_impl,
)
```

## Target Kind Schema

`target_kind(...)` declares the public contract exposed by `once query schema`
and by [Model Context Protocol](https://modelcontextprotocol.io/) target kind
discovery.

- `kind`: optional override for the target kind used in `once.toml`.
  When omitted, Once uses the exported symbol name.
- `docs`: short human-readable target kind description.
- `attrs`: `attr(...)` declarations. Supported types include `string`,
  `bool`, `int`, `float`, `list<string>`, `map<string, string>`,
  `target`, and nested values used by `select`.
- `deps`: `dep(...)` declarations that name expected providers.
- `providers`: provider names this target kind can return.
- `capabilities`: command surfaces this target kind supports, such as
  `build`, `lint`, `run`, `test`, or `metadata`.
- `tools`: `tool(...)` declarations for workspace tools the implementation
  needs during analysis or execution.
- `examples`: `example(...)` declarations for starter workspaces exposed
  to agents.
- `source_references`: `source_reference(...)` declarations that connect this
  target kind to authoritative external rules, plugins, registry records, or
  build-system concepts.
- `impl`: optional function that declares actions and returns a provider
  record.
- `resolver`: optional function that reads declared text inputs and returns
  synthetic targets before graph validation and scheduling.

Attributes can be configurable unless their schema sets
`configurable = False`. Non-configurable attributes reject `select`
during validation before the implementation runs.
Set `implemented = False` on compatibility attributes that should remain
discoverable but are not available yet. Validation returns
`unimplemented_attr` with an attribute-scoped repair before analysis starts.
Values declared with the `target` type use the same package-relative target
syntax as dependency references. Complete-workspace validation confirms that
each referenced target exists and returns an attribute-scoped repair when it
does not.
Use `allowed_values = ["first", "second"]` on a `string` or `target` attribute
when the schema accepts a fixed set. Validation reports an attribute-scoped
repair before analysis when a manifest supplies another value.
Use `disallowed_values = ["", "reserved"]` when the schema accepts arbitrary
strings except for a small reserved set. Validation ignores surrounding
whitespace when comparing these values and reports an attribute-scoped repair
before analysis.
Use `min_count` and `max_count` on `dep(...)` when a dependency role requires
a bounded number of targets. For example,
`dep("deps", ["application"], min_count = 1, max_count = 1)` requires exactly
one default dependency and reports an attribute-scoped repair before analysis.

## Native Project Discovery Contract

`native_project(...)` lets a module recognize an ecosystem-native integration and
map it to one ordinary seed target. The seed target's resolver emits the
detailed graph through the same typed resolver contract as
manifest-authored targets.

```python
native = native_project(
    name = "native",
    target_kind = "native_workspace",
    target_name = "native",
    docs = "Recognizes a native integration.",
    markers = ["project.native"],
    inputs = ["project.lock", "config/**/*"],
    exclude = ["build"],
    input_exclude = ["build", ".git"],
    on_match = "stop",
    max_depth = 16,
    requires_tools = ["native-tool"],
)
```

- `target_kind` names the target kind instantiated by the seed. Schema loading
  rejects a native integration that references an unknown target kind.
- `markers` lists files that must all exist in one directory. The first entry
  drives the bounded scan.
- `inputs` adds optional text globs to the seed's resolver inputs. Markers are
  always included.
- `target_name` names the ephemeral seed target.
- `exclude` lists directory names skipped during discovery.
- `input_exclude` lists directory names that cannot hold a resolver input, so
  gathering the seed's inputs does not descend into them. This is a different
  question from `exclude`: a vendored dependency directory is excluded from
  discovery, because it is not a project of its own, and yet its manifest is an
  input to the project that vendored it. A build output tree or a version
  control directory is neither, and walking one to look for inputs is the
  largest avoidable cost of loading a large workspace.
- `on_match = "stop"` keeps the shallowest matching root, which suits native
  workspaces that own nested package manifests. `"descend"` recognizes every
  nested file marker. `"all"` recognizes every matching directory, including
  directory-spanning markers such as `*.xcodeproj/project.pbxproj`.
- `max_depth` bounds discovery.
- `requires_tools` reports executables needed when the seed resolver runs.
- `workspace_markers` optionally lists literal paths, such as `.git`, that
  identify a containing workspace. For configuration-free `build` and `test`
  invocations in a matching native project, Once uses the nearest ancestor
  containing one of these paths as the filesystem boundary. Only that native
  project family beneath the invocation directory supplies seeds, so unrelated
  native integrations elsewhere in the repository do not block the command.
  Run, lint, and `--all` use the same boundary. Selection requires a marker
  directly in the selected project directory. A child project cannot change a
  repository-root invocation without an explicit target. Package-relative native
  targets select their owning project; sibling native seeds remain selectable.
  Marker files and directories both count. Without a containing marker, the
  invocation directory remains the boundary. Existing `once.toml` manifests
  take priority. The default is `[]`, preserving ordinary workspace discovery.
- `owns_descendants = true` makes a matched workspace root take precedence over
  other native project seeds beside or below it. Use this for a native
  workspace whose checked tree intentionally contains another package format,
  examples, or third-party projects that belong to the outer build.

Detection reads file names only. It does not evaluate executable manifests or
invoke native tools. Loading the graph runs the seed target's resolver through
the normal trusted analysis boundary.

Discovery retains at most 16 mebibytes of match records. Once stops with an
error if a workspace produces more, rather than allowing repository size to
drive unbounded memory growth. Narrow the workspace include or exclude patterns,
or use `on_match = "stop"` when nested projects belong to one workspace.

Native discovery provides ephemeral seeds in workspace roots that have no explicit Once
targets. Explicit targets take precedence only in their own package, so an
unrelated root target does not hide native roots elsewhere in a monorepo.
The configured workspace include and exclude patterns still define the
boundary.

Inspect the automatically derived graph through the ordinary query surface:

```sh
once query workspace
once query targets
```

Discovery never writes `once.toml`. Resolver-emitted dependency and product
targets remain derived from the native manifest and lockfile. Model Context
Protocol callers use `once_query_workspace` and `once_query_targets`.

## Dependency Resolver Contract

A target kind resolver imports an authoritative locked dependency graph. It is
not a replacement for the ecosystem package manager. Cargo, Mix and Hex, Swift
Package Manager, Zig, or another native tool continues to own manifest
semantics, registry behavior, version selection, and lockfile updates.

Compiled package outputs must retain compiler-generated resources alongside
static package resources. Restoring a cached package must restore both, without
requiring generated files to remain in the dependency's source checkout. Static
resource staging must not replace directories containing compiler outputs.

The resolver receives a restricted context:

- `ctx["label"]`: package, name, and stable target identifier.
- `ctx["attr"]`: typed owner attributes. `ctx["attrs"]` is an equivalent
  spelling for resolver compatibility.
- `ctx["configuration"]`: normalized target operating system, architecture,
  and ordered selection tokens.
- `ctx["srcs"]`: declared build source patterns.
- `ctx["files"]`: resolver input files decoded as text, keyed by path relative
  to the owner package. Non-empty `resolver_inputs` patterns define this map.
  When the list is empty or omitted, `srcs` is used for compatibility.

Resolver files are limited to 16 mebibytes per file and 64 mebibytes in total
for one target. Use `resolver_inputs` to keep binary, generated, or large build
sources out of this text-only context while retaining them in `srcs` for build
actions. A resolver should consume bounded text such as a manifest, lock file,
or checked-in graph snapshot.

It may use generic host discovery primitives, including `host_command`, to ask
the native package manager for locked metadata. It cannot declare actions or
outputs. Resolution happens while the graph loads, so the returned value must
be deterministic for the declared files, target attributes, module source, and
resolver toolchain.

The compact return form is a list of target records. The detailed form also
chooses owner roots and adds resolver-owned attributes to the owner:

```python
def _resolve(ctx):
    return {
        "targets": [
            {
                "name": "package-1.2.3",
                "kind": "ecosystem_package",
                "deps": ["./transitive-4.5.6"],
                "dependencies": {
                    "build_deps": ["./build-helper-2.0.0"],
                },
                "srcs": ["third_party/package-1.2.3/src/**"],
                "attrs": {
                    "version": "1.2.3",
                    "content_hash": "...",
                },
            },
        ],
        "roots": ["package-1.2.3"],
        "attrs": {
            "resolution_state": "locked",
        },
    }
```

Each target record accepts only `name`, `kind`, `deps`, `dependencies`, `srcs`,
`visibility`, and `attrs`. Dependency references and exact visibility entries
use the same syntax as a manifest, including `./name` for a target in the owner
package. A bare root name that matches an emitted target is also package-local.
When `roots` is omitted, every emitted target becomes an owner dependency.
Resolver-owned and synthetic attributes must be declared in their target kind
schemas, including internal bookkeeping attributes. A resolver attribute
cannot replace a value declared by the owner target. Expansion stops at
100,000 workspace targets so a recursive resolver cannot grow the graph
without bound.

Built-in native workspace resolvers record first-party test target names in
the non-configurable `_default_test_roots` attribute. Targetless `once test`
and native command compatibility adapters use that metadata to avoid running
test suites that belong only to resolved dependencies. A custom resolver can
use the same convention by declaring the attribute as `list<string>` on its
seed target kind and returning the generated test names through `attrs`.

A resolver can also return a non-configurable `_display_name` string for the
owner. Generic run reporting uses it as the user-facing label without knowing
which target kind produced it. When absent, reporting uses the target label.

Resolvers should preserve the ecosystem's authoritative lockfile and source
selection. They may invoke the native package manager when a missing lockfile
must be created or pinned sources must be materialized, and should surface its
failure without inventing a parallel resolution. Modules may use the
[PubGrub version-solving algorithm](https://github.com/dart-lang/pub/blob/master/doc/solver.md)
for an ecosystem that has no authoritative resolver, but it is not the primary
integration boundary.

See [Ecosystems: Import Locked Third-Party Graphs](/guide/graph/ecosystems#import-locked-third-party-graphs)
for the shared update, source-materialization, query, and first-party consumer
workflow.

## Tool Requirements

`tool(name, executables = [])` adds a tool requirement to the target kind
schema and to every loaded graph target of that kind. `name` matches a key in
the workspace `mise.toml`. `executables` lists the commands the target kind may
invoke and defaults to the tool name.

```python
rust_binary = target_kind(
    docs = "Builds a Rust executable.",
    tools = [tool("rust", executables = ["rustc", "cargo"])],
    capabilities = [capability("build", ["binary"])],
    impl = _rust_binary_impl,
)
```

The requirements are returned by `once query schema` and `once query targets`,
so scheduling can collect the complete tool set as soon as the graph is
loaded. Script targets derive the same requirement from their declared
runtime.

Once carries a fixed mise version with each release. On first use it downloads
the matching mise release binary, verifies its published checksum, and stores
it in Once's data directory. A developer-installed mise is never required.
When a graph build session starts, Once resolves declared executable names from
the workspace tool environment and attempts installation when a tool is not
ready. If workspace resolution fails, an available host executable can be used
instead. Mise shims are not treated as concrete tool executables, so a shim
cannot silently route an action back through a developer's global configuration.
Command actions and scripts then run through `mise exec` with implicit
installation disabled. Once authorizes the selected workspace configuration for
these managed invocations while keeping user-global mise configuration isolated.

## Starter Examples

`examples` points at runnable starter bundles for a target kind. Each declaration
provides the example slug, title, selection hint, and package-relative path. The
bundle itself is a real workspace directory with manifests and sources.
Use examples when a caller should be able to discover the target kind, choose a
starter by intent, and materialize a working target without reading
prose docs.

`example(slug, name, use_when, path = None)` defaults `path` to
`examples/<slug>`. Paths are resolved relative to the module package,
validated during schema loading, and loaded only when a caller requests
that specific example.

```python
apple_library = target_kind(
    docs = "Compiles Swift, Objective-C, C, and C++ sources into a linkable Apple module.",
    attrs = [
        attr("platform", "string", required = True, docs = "Apple platform"),
    ],
    providers = ["apple_linkable", "apple_module"],
    capabilities = [capability("build", ["default"])],
    examples = [
        example(
            "apple-library-minimal",
            name = "Minimal Apple library",
            use_when = "You want a small Swift static library.",
        ),
        example(
            "apple-library-with-objc",
            name = "Apple library with mixed Swift and Objective-C",
            use_when = "Your Swift interface calls into Objective-C sources.",
        ),
    ],
    impl = _apple_library_impl,
)
```

`once query target-kinds` lists the available target kinds with their starter
example slugs. Use `once query schema <kind> --format json` for the full
target kind contract and lightweight example descriptors:

```sh
once query schema apple_library --format json
```

```json
{
  "kind": "apple_library",
  "examples": [
    {
      "slug": "apple-library-minimal",
      "name": "Minimal Apple library",
      "use_when": "Start a small Apple library target."
    }
  ]
}
```

Agents should use `use_when` to pick the closest starter, then materialize it
without copying file payloads through model context:

```sh
once edit materialize-example apple_library apple-library-minimal
```

Fetch the file bundle only when the caller needs to inspect or adapt it:

```sh
once query example apple_library apple-library-minimal --format json
```

```json
{
  "slug": "apple-library-minimal",
  "name": "Minimal Apple library",
  "use_when": "Start a small Apple library target.",
  "files": [
    {
      "path": "apps/Hello/once.toml",
      "contents": "[[target]]\nname = \"Hello\"\nkind = \"apple_library\"\n..."
    }
  ]
}
```

Text files use `contents`. Binary files use `contents_base64` so direct
materialization preserves their exact bytes.

[Model Context Protocol](https://modelcontextprotocol.io/) callers use
`once_list_target_kinds` and `once_query_schema` for discovery, then
`once_materialize_example` for direct setup or `once_query_example` to inspect
the file bundle. Repository contributors can run
`mise run examples:verify-portable` to materialize and execute the portable
starter set.

## External Rule Assimilation

A project module can reproduce the useful portion of a rule or plugin that
Once has never seen before. The coding harness owns the translation and keeps
it with the project. Once supplies the typed schema, validation, generic action
primitives, execution, cache, and evidence surfaces.

Start by fetching the authoritative source:

```sh
once query external-source https://example.com/rules/write_file.star --format json
```

The fetch accepts public HTTPS text, does not follow redirects, and returns at
most the requested byte limit. Its digest lets the harness remember which
upstream content it interpreted. Use a final raw source, registry, or
documentation address instead of an unbounded repository page.

Query the authoring contract instead of assuming that a remembered primitive
signature is still current:

```sh
once query module-contract --format json
```

The result contains the declaration helper source, schema invariants,
implementation context, analysis primitives, action primitives, maintenance
invariants, module registration, and runnable starters. For test modules it
also returns a matching target table and an exact normalized result example.
Attribute defaults in schemas are descriptive strings and do not insert
runtime values. An implementation uses `ctx["attr"].get(...)` when an optional
attribute needs a fallback. After writing the project module, validate it in
isolation:

```sh
once query validate-module modules/generated_text.star --format json
```

Attach the upstream relationship to the local target kind:

```python
generated_text = target_kind(
    docs = "Generates one text file from declared lines.",
    attrs = [
        attr("output", "string", required = True, configurable = False),
        attr("lines", "list<string>", required = True),
    ],
    providers = ["generated_file"],
    capabilities = [capability("build", ["default"])],
    source_references = [
        source_reference(
            "Example Build",
            "write_file",
            "https://example.com/rules/write_file.star",
            "Replicate the requested generated-file node and no unrelated graph nodes.",
            content_digest = "digest returned by once_fetch_external_source",
        ),
    ],
    impl = _generated_text_impl,
)
```

`source_reference(...)` is descriptive metadata, not executable trust. It is
returned by target kind discovery so a future maintenance pass can refetch the
source, compare its digest, and decide whether the local translation needs an
update. Set `content_digest` only after a complete, untruncated fetch. Omit it
for documentation-only references.

Translate only the dependency closure necessary for the user's requested
capability. Keep everything else in the source build. Invoke tools directly
with argument lists, declare inputs and outputs, and use portable setup
primitives instead of hiding graph work in a shell command. Then validate the
module, target tables, and complete workspace before executing the capability
and checking fresh evidence.

## Implementation Context

An implementation receives `ctx` with generic graph data:

- `ctx["label"]`: `package`, `name`, and stable `id`.
- `ctx["attr"]`: typed attributes after manifest parsing.
- `ctx["srcs"]`: raw source patterns from the target.
- `ctx["deps"]`: provider records returned by analyzed dependencies.
- `ctx["deps_by_role"]`: provider records grouped by the dependency roles
  declared in the target kind schema. The `deps` entry contains the same
  providers as `ctx["deps"]`.
- `ctx["build_dir"]`: workspace-relative output directory for the
  target.
- `ctx["scratch_dir"]`: workspace-relative scratch directory for
  action-private helper files that are materialized before an action
  runs but are not durable target outputs.
- `ctx["capability"]`: active capability being analyzed.
- `ctx["run"]`: run request options. `ctx["run"]["visible"]` is true when
  the caller requested a visible runtime interface. `ctx["run"]["args"]`
  contains caller arguments for the run capability and is empty for other
  capabilities. Their interpretation is target-kind-specific.
- `ctx["test"]`: test request options. `ctx["test"]["filters"]` contains
  stable semantic unit identifiers selected for this test execution. Target
  kinds that declare case filtering translate these identifiers into native
  runner arguments. `ctx["test"]["batch_id"]` is a stable batch identifier
  during automatically scheduled execution and is `None` for a normal
  whole-target execution.

The implementation returns a dictionary of provider fields. Downstream
target kinds should read provider fields from `ctx["deps"]` or
`ctx["deps_by_role"]` instead of inspecting target identities.

A resolver or implementation reports an actionable failure by calling `fail` with the text
`once.diagnostic.v1 ` followed by one JSON object holding `code`, `message`,
and optionally `attribute` and `repairs`. The engine attaches the target and
returns a structured diagnostic with those fields, so agents receive the same
`code`, `attribute`, and `repairs` shape as schema validation. Any other
failure text is reported as a generic analysis failure. Actionable resolver
diagnostics stay attached to the resolver owner rather than preventing unrelated
targets from loading. Selecting that owner or a dependent target still fails with
the diagnostic and its repairs. Unexpected resolver evaluation failures remain
fatal to graph loading.

When an artifact has different link-time and runtime dependency closures,
publish those closures as separate provider fields. Do not flatten them into
one transitive list. Runtime bundle records should retain the owning target,
bundle path, module name, and complete file outputs so final packaging actions
can de-duplicate, materialize, and sign the closure without rediscovering the
graph.

## Host Globals

Target kind implementations can use these generic primitives:

Modules are trusted analysis code. `host_command` executes on the host while
the graph is loading, so load modules only from trusted sources and reserve it
for deterministic discovery. It must not mutate workspace sources or the build
output tree, and resolver scratch state belongs only under `.once/tmp`. Prefer
declared lock files and checked-in graph snapshots when a package manager can
supply them. Build work and source fetching belong in explicit actions or a
separate update workflow.

- `host_arch()` and `host_os()` return normalized host identifiers.
- `host_env(name)` returns one host environment variable, or an empty
  string when it is unset.
- `workspace_root()` returns the absolute workspace root.
- `host_which(name)` resolves an executable on `PATH`.
- `host_command(argv, env = {}, cwd = None, merge_stderr = False, check = True, max_output_bytes = None)`
  runs a discovery command and returns standard output. A non-zero exit status
  fails analysis unless `check = False`, which returns the captured output
  for idempotent setup that a later probe verifies. Arguments, environment
  values, the working directory, stream merging, `check`, and the output limit
  participate in the command-scoped cache key. When set, `cwd` must be an
  absolute path, normally derived from `workspace_root()`. Standard input is
  closed: discovery must not prompt or consume the caller's input. The
  captured output is limited to 16 mebibytes by default. Pass
  `max_output_bytes` to raise that bound for a command whose answer is
  inherently large, such as a build system's dependency-closure action graph;
  the bound applies while capturing each stream and to the merged result,
  and is recorded with the observation so later validation re-runs the
  command under the same limit.
- `host_symlink_target(path)` returns where a host symbolic link points, as
  written, or an empty string when the path is not a link. A planner uses it to
  declare the files a linked input depends on.
- `host_file_size(path)` returns the size in bytes of a host file, which a
  deferred planner uses to describe an artifact produced by an earlier action.
- `host_file_exists(path)` checks whether a host path is currently a
  file.
- `host_path_exists(path)` checks whether a host path currently exists,
  including directories and symbolic links. Use it when a resolver probes a
  source tree rather than reads a regular file.
- `host_path_is_within(path, root)` canonicalizes two existing host paths and
  reports whether the first is contained by the second. Symbolic links are
  resolved before the containment check.
- `host_file_read(path)` reads a host file as
  [Unicode Transformation Format, 8-bit (UTF-8)](https://www.unicode.org/faq/utf_bom.html#UTF8)
  text. Analysis rejects files larger than 16 mebibytes.
- `host_file_sha256(path)` returns a host file's
  [Secure Hash Algorithm 256-bit](https://csrc.nist.gov/pubs/fips/180-4/upd1/final)
  digest as lowercase hexadecimal text.
- `host_file_contains(path, needle)` checks host file text content and applies
  the same 16 mebibyte file limit.
- `glob(patterns, exclude = [])` expands patterns under the active package,
  omits matches selected by package-relative exclude patterns, and returns
  sorted workspace-relative file paths.
- `walk_files(root, excluded_paths = [], excluded_names = [], include_empty_directories = False)`
  walks a package-relative directory and returns sorted workspace-relative file
  and symbolic-link paths. Exact root-relative exclusions and file names prune
  whole trees before traversal. With `include_empty_directories = True`, every
  directory with no remaining entries is returned as well, so a consumer that
  copies the tree keeps it.
- `walk_symlinks(root, excluded_paths = [], excluded_names = [])` walks a
  package-relative directory and returns one string per symbolic link, the
  link's workspace path and its target joined by a NUL character. It follows
  no link, so a planner learns every link and destination with one walk.
- `walk_workspace_files(root, excluded_paths = [], excluded_names = [])` walks
  a workspace-relative directory without following symbolic links. Use it
  when a resolver must inspect a path shared by targets in different packages
  or explicitly snapshot a symbolic link whose destination is host managed.
- `declare_output(name)` reserves an output under the target build
  directory.
- `execution_path(path)` returns a stable command value for a
  workspace-relative path. Once resolves it against the local, sandbox, or
  remote execution root immediately before launching the process. Use it for
  tools that require absolute paths without embedding a host workspace path in
  the action digest.
- `cmd_args(args, use_arg_file = None)` creates a structured
  command-line fragment. `args` is a list of strings. When
  `use_arg_file` is set, it is a dictionary with `path` plus optional
  `format` and `arg_format`. The supported `format` value is
  `line-delimited`, which writes one argument per line without shell
  escaping. The caller chooses `path`; use `ctx["scratch_dir"]`
  for action-private helper files and `declare_output` for durable
  target outputs. `arg_format` defaults to `@{}` and must contain
  exactly one `{}` placeholder.
- `run_action(...)` records a command action for Once to execute. Its optional keyword-only `history` reports a producer-owned logical identity for cross-run history, independently of execution and cache identity.
- `action_history_key(namespace, components)` creates a bounded opaque descriptor for `run_action(history = ...)`. Use a versioned ASCII namespace (1–64 bytes) and 1–8 string components (2048 bytes total, no controls). Components are encoded as a canonical JSON array and SHA-256 hashed. Choose a stable owner, native operation and structural output variant; never use run IDs, action indices, cache digests, source revisions, display labels, absolute paths or credentials. Invalid optional metadata is omitted without failing analysis. Deferred children need their own explicit keys, while cached outcomes retain recorded keys. Older actions stay ungrouped.

Built-in history currently covers Rust compilation/build scripts/test-runner compilation, Go compilation, Kotlin native/JVM/test compilation, Zig compilation and delegated CMake builds. Other actions still have occurrence details but do not claim cross-run equivalence. Rust registry/git dependencies use resolver-directory, resolver logical label and source identity without revision/requested-ref or userinfo, package and native target names, a Cargo-compatible version bucket, phase, output triple, host-tool role and mobile variant/consumer. Crates.io index protocols normalize to one origin. Stable `1.x` versions share a bucket, `0.x` versions use the minor range, and `0.0.x` and prereleases stay exact. Workspace labels are version-independent; source-less generated dependency origins remain ungrouped. Target renames and incompatible versions start new histories. Producer derivation changes require a namespace version bump. The server omits ambiguous within-run keys from historical comparisons and qualifies first observations using retained reports.
- `write_path(path, content)` materializes generated text or byte-list
  files through normal actions.
- `copy_path(source, destination, inputs = [])` copies one workspace
  path by value. A directory symlink is materialized at the destination.
  Source paths are always action inputs; `inputs` adds any other paths read
  while deciding what to copy.
- `copy_path(source, destination, kind = "tree", inputs = [])` copies
  one or more directory contents while preserving their symlink layout. Each
  source tree is automatically hashed as an input.
- `materialize_host_file(source, destination)` snapshots one absolute host
  toolchain file into a workspace output. Analysis records its
  [256-bit Secure Hash Algorithm digest](https://csrc.nist.gov/pubs/fips/180-4/upd1/final),
  and execution verifies the digest before the output enters the cache.
- `materialize_host_tree(source, destination)` snapshots one absolute host
  directory into a workspace directory output. Analysis and execution verify
  its content, file modes, and symbolic links before the output enters the
  cache.
- `link_path(source, destination)` declares an uncached, relative workspace
  link. The source must exist and is automatically declared as an input.
  Linked contents are not copied into the action cache, while downstream
  actions still hash them when they declare the link as an input. Windows
  hosts require permission to create symbolic links.
- `prepare_path(path, kind = "remove")` and
  `prepare_path(path, kind = "directory")` declare uncached cleanup and
  setup actions for workspace paths.
- `write_tree_digest(root, output, include_suffixes = [])` writes a
  deterministic digest listing for a workspace tree. The root is
  automatically declared as an input.
- `write_archive(entries, output, sha256_output = None, format = "tar", uncompressed_sha256_output = None)`
  writes a deterministic
  [tar archive](https://www.gnu.org/software/tar/manual/html_node/Standard.html),
  uncompressed by default. Each entry declares a `file`, `directory`,
  recursive `tree`, or `symlink` (with a `target`), its archive path, fixed
  mode, numeric owner and group identifiers, and modification time. When
  requested, `sha256_output` records the
  [Secure Hash Algorithm 256-bit](https://csrc.nist.gov/pubs/fips/180-4/upd1/final)
  digest. With `format = "tar.gz"` the archive is compressed with a gzip header
  that carries no timestamp, name, or operating system, so identical entries
  produce identical bytes on every machine; `sha256_output` then digests the
  compressed bytes and `uncompressed_sha256_output` digests the tar inside.
- `download_and_extract(url, sha256, destination, authorization_env = None)`
  streams a checksum-pinned [ZIP archive](https://www.loc.gov/preservation/digital/formats/fdd/fdd000354.shtml)
  into a declared directory output. An optional environment-variable name
  provides a web Authorization header only while the action executes, so its
  value is excluded from action declarations, cache keys, logs, and evidence.
  Runtime policy defaults to a 4 GiB downloaded archive and one million entries.
  Expanded regular-file data is capped at 4 GiB, or 64 times the declared download
  length when that is smaller. Symlinks and directories have separate metadata
  and entry-count limits; this is not a hard filesystem-space ceiling. Highly compressible archives exceeding this bound are
  refused. Archive bytes and the expansion bound are reserved together, so small
  known-length downloads can run concurrently without an extraction admission
  cycle. Unknown-length downloads reserve the full worst case and can serialize.
  Downloads share admitted staging, have a
  30-second progress-stall bound, and must finish within 30 minutes, including
  staging waits and private archive writes. Checksum validation and extraction
  follow download completion; extraction retains admission until its physical
  filesystem work ends rather than being treated as cancellable.
  ZIP metadata is checked before decoder allocation. Multi-disk archives,
  prefixed data, trailing bytes, inconsistent ZIP64 records and excessive
  metadata are refused. Embedded end records with excessive entry counts may
  also be refused to prevent decoder fallback from bypassing the limits.
- `toml_decode(src)` and `json_decode(src)` decode data into Starlark
  values.

Portable file actions derive their cache keys from their operation and consumed
inputs, not from unrelated actions declared earlier by the same target. Copy,
link, tree-digest, and archive actions infer their direct source inputs. Use the
`inputs` argument for additional paths that influence the operation. Portable
actions still execute in declaration order, and Once hashes generated inputs
directly. Blocking file operations and output capture own their scheduling
reservations until the work stops, even when the caller is cancelled. A cancelled
observer does not publish a completed action-cache entry for partial work.

Use these primitives to express resolver commands, source filtering, file
formats, tool selection, compiler flags, provider conventions, and action
layout.

## Actions

Native adapters can use the Apple kinds' `prebuild_actions`,
`prepackage_actions`, and `postbuild_actions` attributes for ordered serialized
script declarations. Preparation actions precede compilation or resource
processing. Pre-package actions run after linking but before resource processing.
Shell script records execute in the workspace so absolute native settings and
in-place product edits agree with the declared output locations. Their caching
still depends on complete input and output declarations; it does not imply
filesystem isolation.
They can generate resource files and declared resource-bundle trees. Post-build
actions follow product assembly and precede final signing. Records carry a
shell or argument list, environment, workspace-relative inputs and outputs,
working directory, and an explicit cacheability decision. Native Xcode
records also retain their phase identifier and original build directory so
the target kind can map environment values and paths to the configured
execution directory. Scripts without complete declarations must bypass the
cache. After an untracked product script, the bundle tree is captured again
so later actions consume modified and newly created files and respect deletions.
A replaced output tree supersedes earlier records for its children. Stale
top-level signatures are removed before these scripts; final
signing and subsequent scripts consume the updated tree.
Native records may carry `unresolved_file_lists` to retain bootstrap-dependent
phases during import; executing such a record fails until those settings resolve.

`run_action` accepts:

- `argv`: command and arguments as strings or `cmd_args` fragments.
- `inputs`: workspace-relative files and directories hashed into the
  action digest.
- `outputs`: workspace-relative outputs the action must produce. Once creates
  their parent directories. If a previous action version left a file where a
  managed output now requires a directory, Once replaces the stale file.
  Later directory outputs supersede older child records. Later child outputs
  override earlier directory snapshots during restoration.
- `clean_paths`: workspace-relative paths to remove before a fresh
  command execution. Cache hits restore outputs without running the
  command.
- `create_dirs`: workspace-relative directories to create before a fresh
  command execution.
- `cwd`: workspace-relative directory to run the command in. Defaults to
  the workspace root when omitted or `None`.
- `env`: string environment variables.
- `sandbox`: local filesystem sandbox policy. `off` uses the current
  workspace view. `inputs` runs in an action-private workspace view
  populated from declared inputs and copies declared outputs back after
  a successful command. `copied-inputs` provides the same isolation but
  materializes private input copies for tools that cannot consume symbolic
  links. Both isolated modes create parent directories for declared outputs
  inside the private workspace. Use `once query validate-actions` to run an
  uncached contract investigation that checks created, modified, and deleted
  paths and returns structured repairs. Reads that leave no observable
  filesystem evidence remain a limitation of this investigation.
- `success_exit_codes`: integer list whose members mean the command completed
  and its declared outputs are valid. Defaults to `[0]`. Use this for tools
  that report valid findings with a nonzero process code. Accepted codes are
  normalized to success. If the list excludes `0`, a process exiting with `0`
  fails instead of being cached as successful. After upgrading, actions whose
  list excludes `0` run once more instead of reusing earlier results. Policies
  including `0` keep their action-cache keys.
- `cacheable`: `True` by default. Set `False` for interactive or local
  side-effect actions. Uncacheable commands that run directly in the workspace
  (`sandbox = "off"`, without network denial) record directory and symlink outputs
  as fingerprints, not restorable contents.
  Keep these outputs unchanged until their consumers finish. If an output needs
  restoration after cleanup or replacement, Once refuses rather than installing
  its fingerprint as file contents. Make the producer cacheable or use an input
  sandbox when consumers need restorable outputs.
  Consumers of these fingerprint-only outputs execute afresh, even when their
  own `cacheable` setting is `True`, because older cached results may contain
  fingerprint text instead of the original contents. This also applies to
  implicit same-target inputs through `depends_on_prior_actions`. Fresh results
  still use normal cache publication; ordinary file and restorable artifact
  inputs retain cache reuse. A raw file with bytes exactly equal to an earlier
  fingerprint at the same path cannot be restored during that invocation.
- `inherit_parent_env`: `False` by default. Set `True` only on an
  uncacheable, unsandboxed local run action that should behave like a command
  launched from the user's development shell. Explicit `env` values take
  precedence.
- `depends_on_prior_actions`: `True` by default. When true, each action key
  includes prior actions declared by the same target and acts as an ordering
  barrier. With `False`, declared input and output paths determine ordering;
  every consumed generated file must be listed in `inputs`. On a cache miss,
  unrelated cached outputs remain deferred until an ordered action or a requested
  output needs them. Declared filesystem changes that overlap cached outputs,
  including uncertain path aliases, retain conservative restoration before
  execution. This does not change action keys.
- `intermediate`: `False` by default. Set `True` on an action whose outputs only
  later actions of the same target read, such as a step that feeds the next
  one. When the action is a cache hit, its outputs stay in the cache instead of
  being written to disk at the end of the target, and are restored only when a
  later action that reads them has to run.
- `toolchain_identity`: optional string folded into the action digest.
- `identifier`: stable diagnostic label.
- `display_name`: optional human-readable name, such as `Compile main.c` or
  `Link application`. This keyword-only presentation field does not replace
  `identifier` or change the action's cache key. Do not include secrets,
  command arguments, or absolute host paths in names. Control characters are
  normalized to spaces to keep names on one line.
- `source_files`: optional keyword-only list of repository-relative file paths
  associated with the action. One file uses a one-element list; an action
  operating on several files can list each one. Omitted or `None` means no
  source links. Paths use forward slashes and cannot contain absolute paths,
  drive prefixes, backslashes, empty segments, `.` or `..` segments, or control
  characters. These are links, not additional inputs or outputs.

All executable action primitives, including portable file actions, accept
`display_name`, `source_files`, and `presentation` as optional keyword arguments. Portable file
operations supply readable default names when no name is given. Bundled target
kinds give compilation, linking, testing, packaging, and setup work descriptive
names. Use `source_files = _action_source_files(paths)` to supply a descriptor
of workspace source paths. Once resolves this presentation-only descriptor
while collecting action metadata, memoizing file selection and repository
mapping without adding execution observations or invalidating cached builds.
It excludes directories, generated `.once` paths, absolute host files, and
nonportable repository paths. Entries must be strings. It maps files relative
to the nearest Git checkout, including worktrees, and honors an explicit
`GIT_WORK_TREE`. Without a checkout, paths remain workspace-relative.

```python
run_action(
    argv = [compiler, "-c", "src/main.c", "-o", object_file],
    inputs = ["src/main.c", "include/api.h"],
    outputs = [object_file],
    identifier = "compile-main",
    display_name = "Compile main.c",
    source_files = ["src/main.c"],
)
```

The optional `presentation` record describes ecosystem-native context without
changing execution or cache identity:

```python
presentation = _action_metadata(
    package = _action_package("custom", "library", version = "1.2", origin = "registry"),
    platforms = [_action_platform("custom", "native-target", label = "Target platform")],
    context = [_action_context("custom.mode", "release", label = "Release")],
)
```

A package may contain `ecosystem`, `name`, `version`, `revision`, `digest`, and
`origin`. A platform has a stable `scheme` and `id`, an optional display `label`,
and optional `usage` (`product` or `build-tool`). A context entry has a stable
namespaced `key` and `value`, and an optional display `label`. All fields default
to empty; omit irrelevant concepts. Declared output platforms are not execution
worker placement. Versions, VCS revisions, content digests, SDK levels, and
compiler versions are separate concepts. Never copy raw environment maps,
credentials, authenticated URLs, or host paths into presentation.

Once retains at most eight platforms and eight context entries, with at most
2048 bytes of string data in total. Names are bounded to 128 bytes, namespace
keys to 64 bytes, identifiers and values to 256 bytes, and display labels to
128 bytes. Invalid or oversized identifiers are omitted rather than truncated;
malformed optional records are ignored rather than failing execution. Labels
normalize control characters to spaces and oversized labels are omitted.
`expand_actions(..., presentation = ...)` passes platforms and context to
children that do not declare their own metadata, but never inherits package
ownership into unrelated generated actions.

Bundled producers select only owned facts: Cargo package versions and compiler
triples, Apple output triples, Go OS/architecture and effective cgo settings,
Kotlin JVM levels, Android API levels and native ABIs, Zig's effective optimize
mode, OCI platform sets and immutable digests, Mix environment and dependency
identity, and known test/task/build context. Mix registry dependencies use the
`hex` ecosystem and a package version; Git/local dependencies use `mix`, with a
Git revision or no version respectively. Opaque CMake/Bazel/React Native steps
never claim invisible internal package graphs or worker platforms. Shell/file
operations, runtime test runners, and other actions may correctly omit package
or platform information. Cargo's ambiguous workspace `0.0.0` default is omitted
unless an explicit package version is available. Cargo resolvers annotate
host-tool outputs with the schema-declared `_cargo_host_tool` boolean. This
marker controls presentation and logical-history role, not scheduling or execution placement. The optional, nonconfigurable `_cargo_resolver` string records the resolver's logical label so separate resolver declarations in one package keep distinct histories, including host tools. It does not enter execution or cache identity.

Once includes this metadata in per-action completion events and preserves it
when replaying cached target outcomes. An event server can show the name and
link each file at the run's Git revision using its own repository integration.
Older servers ignore the additional optional fields; no protocol upgrade or
new required feature is necessary. If optional presentation fields exceed the
server's event-size limit, Once retains the source links that fit and may omit
an oversized display name or structured context. The action's identity, timing, and result are
preserved.

Consumers must include staged output paths in their declared inputs. Mentioning
a path only in arguments or environment variables does not guarantee that a
cached staging action materializes it. Prefer disjoint staged output trees
rather than a parent directory followed by overlapping child copies. If a native
consumer requires a complete build tree followed by application overlays, each
child copy must declare the staged parent as an input so it is restored first.

Optional toolchain and action identities are encoded separately in the input
hash, including presence and embedded separators. Equal concatenated text in
different fields cannot share a cache key. This identity encoding invalidates
older declared-action cache entries once without changing artifact formats.

The declared action is its executable cache contract. Once hashes its operation,
arguments, argument files, environment, working directory, path setup,
toolchain identity, declared input contents, and dependency action digests.
Changing target-kind module source without changing that declaration does not
invalidate the action. A rule edit that changes any declared behavior or input
still produces a new digest.

Consecutive actions with `depends_on_prior_actions = False` may run concurrently
when their declared reads, writes, argument files, and cleanup paths do not
conflict. Uncacheable actions and ordering barriers remain sequential. Both
target and action concurrency respect the command's memory budget. Independent
graph targets become ready after their dependencies complete. Command preparation,
portable filesystem operations, output capture, producer repairs, and cached output
restoration use shared invocation admission. Inputs are restored before admitting
their consumer, and each restore attempt releases its admission before repairing a
missing producer. Cancellation does not release a reservation or action lock while
its blocking filesystem work still runs. These execution guards do not change
cache identity or authorize remote graph placement.

## Deferred Action Planning

`expand_actions(implementation, inputs, outputs, args)` declares a planning
barrier. After preceding actions finish, Once restores its `inputs` and calls
the named exported Starlark function from the same module environment. This
lets a compiler's dependency scan describe further cacheable actions without
running the compiler during initial analysis.

The callback receives `ctx.args`, `ctx.inputs`, `ctx.outputs`, `ctx.build_dir`,
and `ctx.label` (`id` and `package`). It declares ordinary actions and returns
`None` on success, or one structured diagnostic with `code`, `message`,
`target`, `attribute`, and `repairs`. Every promised output must be produced
by a declared action. Recursive expansion is rejected. The callback runs under
the resource budget, and its child actions use normal caching and scheduling.

```python
def finish_plan(ctx):
    scan = json_decode(host_file_read(workspace_root() + "/" + ctx["inputs"][0]))
    run_action(
        argv = [ctx["args"]["compiler"]] + scan["arguments"],
        inputs = scan["inputs"],
        outputs = ctx["outputs"],
    )
```

Initial action queries show the scan and planning barrier, not the expanded
commands. Isolated contract validation reports this limitation rather than
running prerequisite builds. Execution evidence contains the expanded actions.
Targets with deferred actions currently replay planning on warm builds instead
of using the whole-target shortcut; their individual actions still reuse cache
entries.

`content_sha256(text)` hashes text without filesystem access.
`host_tree_sha256(path)` hashes an absolute host directory's contents and
structure, using metadata-validated cached digests on later invocations.
It records the tree as an analysis observation and rejects symbolic links
that escape the tree. Entry paths and symlink targets must be valid UTF-8;
invalid text is rejected rather than assigned an ambiguous digest. Literal
backslashes in Unix names remain literal characters, while Windows directory
separators use the portable forward-slash spelling.
Together with `host_file_sha256(path)`, these primitives
let target kinds include host tools and development kits in action identity.
They identify host inputs; they do not make those inputs remotely available.

## Executable And Container Providers

Native binary target kinds can publish the shared `once_executable` provider.
It carries the executable path, runtime files, operating system, architecture,
architecture variant, and linkage. Consumers depend on that provider instead
of branching on the language that produced the program.

The built-in `oci_layer` and `oci_image` target kinds use
[`oci` as the abbreviation for Open Container Initiative](https://opencontainers.org/).
An `oci_layer` packages native executable providers and source files into a
deterministic filesystem layer, optionally gzip-compressed with `compress` and
extended with `symlinks`. It can also pass through an existing uncompressed
layer archive during incremental adoption. An `oci_image`
assembles ordered layers, runtime configuration, a content-addressed image
layout, and an archive accepted by
[Docker](https://www.docker.com/). Neither needs Docker or any interpreter:
the layout is written by ordinary declared actions after the layers exist.
Both native images and Dockerfile-backed images publish the shared
`container_image` provider.

An `oci_image` can extend a `base` dependency, any `container_image` provider
that exports a layout. The new layers, runtime configuration, and history are
appended to the base image's. `entrypoint` resets an inherited `cmd` unless
`cmd` is also set, and `env`, `labels`, `exposed_ports`, and `volumes` merge
with the base. `tag` and `tags` name the image, one layout entry per name.
An `oci_index` combines single-platform images into one multi-platform
index, rejecting two images for the same platform. An `oci_pull` fetches one
image by content digest into a layout, keeping `platform` (or `all`), so the
result is immutable and cacheable. An `oci_import` extracts an existing image layout archive (`.tar`, `.tar.gz`, `.tgz`) with `tar` and exposes it as an image. An `oci_load` target loads an image archive
into a local engine when run, and an `oci_push` target publishes an image or
index when run, keeping its manifest digest. Setting `sign` on an
`oci_push` signs the pushed digest with cosign. Pull and push use
[crane](https://github.com/google/go-containerregistry/blob/main/cmd/crane/doc/crane.md)
and the Docker configuration for credentials; load uses `docker` or `podman`.

```toml
[[target]]
name = "server_layer"
kind = "oci_layer"

[target.dependencies]
programs = ["server"]

[[target]]
name = "server_image"
kind = "oci_image"

[target.dependencies]
layers = ["server_layer"]

[target.attrs]
tag = "server:latest"
cmd = ["serve"]
env = { LOG_LEVEL = "info" }
working_dir = "/app"
```

When a layer contains exactly one executable, the image uses that executable
as its default entry point. Explicit `entrypoint`, `cmd`, `env`, `user`,
`working_dir`, `stop_signal`, labels, annotations, and exposed ports override
or extend the runtime configuration. Layer metadata defaults to zero for the
numeric owner, group, and modification time so identical inputs produce
identical bytes.

The `dockerfile_image` target kind defaults to `execution_mode = "auto"`, which
translates instructions into separate actions and runs one whole-file BuildKit
action when the Dockerfile uses syntax that cannot be translated or an option
that needs whole-file execution. The provider records the resolved
`execution_mode` and any `fallback_reason`, and the `lint` capability reports a
`whole_file_execution` note naming the reason.
Native project discovery creates an `image` target for each `Dockerfile`. The
Dockerfile's directory supplies its build context. Once uses the selected
Buildx builder when it supports image-layout exports. When Docker's built-in
builder is the implicit default, Once provisions and reuses a dedicated
container builder for the current Docker endpoint without changing the
selection. Set `BUILDX_BUILDER` to select a remote builder, or declare a target
to configure a different Dockerfile, context, and caching policy.
Starlark translates instruction boundaries and stage references into separate
actions. Each action consumes declared context files and prior image snapshots,
then produces an [Open Container Initiative image layout](https://github.com/opencontainers/image-spec/blob/main/image-layout.md).
[BuildKit](https://docs.docker.com/build/buildkit/) executes the individual
instruction. `RUN` commands keep their image filesystem, environment, user,
working directory, and shell. Stage-scoped arguments and environment values are
resolved during analysis, following Docker's scope and inheritance rules, and
written into each instruction's definition; an `ARG` default that depends on
the base image's own environment is not translatable. Cross-stage `COPY` and bind mounts declare their source
snapshots as action inputs. Only stages reachable from the selected stage
execute, and archive export is a separate action.
Intermediate layouts preserve filesystem timestamps between instructions;
`source_date_epoch` fixes the image creation time. BuildKit does not rewrite
layers imported from a snapshot, so file timestamps inside the archive reflect
when each instruction ran unless `reproducible_layers = true`, which rewrites
each instruction's layer to `source_date_epoch` as it is exported. Whole-file
mode normalizes the final layers.

Instruction mode requires a running Docker engine with Buildx and nothing
else. Once provisions
a container builder when the selected builder cannot export image layouts.
That builder cannot see base images stored only in the local Docker image
store. Use whole-file `buildkit` mode with Docker's built-in builder and
`pull = false` for those images, or publish the base image to a registry and
pin its digest.

The `images` dependency role passes `container_image` targets to the Dockerfile
as named build contexts, so `FROM name` and `COPY --from=name` resolve to a
content-addressed layout instead of a registry. The `programs` role passes
native executables the same way, as a directory holding the executable under
its file name. A context is named after its target; `context_names` maps a
target to another name, including a registry reference such as `alpine:3.23`,
which then replaces that reference wherever the Dockerfile uses it and counts as
pinned for `cacheable`. `tag` and `tags` name the archive entries. Builds with a
container or remote builder also export a `layout` that other targets can use.

Instruction execution exposes `layout` and `plan` output groups in addition to
`archive` and `metadata`. The plan records source lines, instruction identities, snapshots,
and advisory diagnostics. Metadata queries expose the parsed instructions and
diagnostics without contacting Docker. Literal copy sources are tracked
individually, together with the destinations of any symbolic links they reach.
Wildcards, variable sources, and context bind mounts retain the full context
input set conservatively. Empty directories in the context are inputs, so a
copied tree keeps them. Generated definitions always exclude
Once runtime state from the context.

The `lint` capability uses the shared lint provider and emits warnings with
`code`, `target`, `attribute`, source line, and suggested `repairs`. Checks cover
mutable base references and syntax frontends, copy sources missing from the context, package repositories, downloaded pipelines, and
authentication mounts. Findings are advisory, can have false positives, and
never change the explicit `cacheable` decision. Clean results do not prove
reproducibility.

Set `execution_mode = "buildkit"` to delegate the complete recipe to BuildKit
as one action. This mode supports heredocs, `ONBUILD`, `# syntax=` frontends,
a backtick escape directive, multiple declarations in one `ARG`, and extended
argument expressions. It also owns the `export_cache`, `cache_to`, and `caches`
dependency contracts described below. With `execution_mode = "instructions"`,
unsupported syntax fails with the source line and this setting; `auto` switches
to it instead, and lint and metadata queries never fail on such syntax.
Docker and its selected Docker Buildx builder must be reachable when Once
analyzes a build so the toolchain identity can include the builder driver and
BuildKit version. Metadata and affected-file queries infer context inputs
without contacting Docker.

```toml
[[target]]
name = "service_image"
kind = "dockerfile_image"

[target.attrs]
builder = "container-builder"
tag = "service:latest"
platform = "linux/arm64"
build_args = { MODE = "release" }
```

When `srcs` is empty, Once infers every local file and symbolic link that the
build context can expose, including hidden paths. It also adds the Dockerfile
and the effective [Docker ignore file](https://docs.docker.com/build/building/context/#dockerignore-files)
automatically. Dockerfile-specific ignore files take precedence over the
context's ignore file, matching Docker behavior.

Inference recognizes simple literal ignore paths plus `**/<name>` exclusions
and prunes their trees before walking the context. Wildcards, exceptions, and
other complex ignore rules remain declared as inputs, then BuildKit applies
their complete semantics during the build. This conservative direction can
cause extra rebuilds, but it cannot hide a file that the Dockerfile may read.
Workspace runtime directories named `.once` are always excluded so outputs
never become their own inputs.

Empty directories do not participate in the inferred input set yet. Declare a
placeholder file when a Dockerfile must copy an otherwise empty directory.

Set `srcs` only when a smaller, manually maintained input set is worth the
precision. Once still adds the Dockerfile and effective ignore file. Dockerfile
actions use an input-only workspace view, so an explicit set must cover every
local context file the build may read.

The rule rejects multiple values in `platform`; one target describes one
exported platform. Selecting `network = "host"` also grants BuildKit's matching
host-network entitlement.

Dockerfile actions are not stored in the Once action cache by default because
image tags, package registries, and network instructions may change without a
source edit. BuildKit still reuses its own layer cache. Set `cacheable = true`,
`pull = false`, and an explicit `platform` with a container or remote builder
only when every remote input is immutable, such as base images pinned by digest
and network access disabled. Every base image of a built stage must be pinned
by digest; analysis fails otherwise.
Open Container Initiative export requires a Docker Buildx builder that
supports archive export.

When the selected builder supports direct export, both archive formats are
written without loading the image into the Docker engine. The built-in Docker
driver cannot write an archive directly, so Docker output falls back to
loading and saving the requested tag. That compatibility path uses shared
engine state, leaves the loaded image behind, and always bypasses the Once
action cache. Use a container or remote builder when builds sharing the same
tag may run concurrently.

Set `export_cache = true` to export all intermediate BuildKit layers as a
declared `layer_cache` directory alongside the archive. Once stores and restores
that directory with the other outputs when action caching is enabled. Another
image target can import it through a `caches` dependency:

```toml
[[target]]
name = "base_image"
kind = "dockerfile_image"

[target.attrs]
execution_mode = "buildkit"
dockerfile = "Dockerfile.base"
builder = "container-builder"
export_cache = true

[[target]]
name = "service_image"
kind = "dockerfile_image"

[target.dependencies]
caches = ["base_image"]

[target.attrs]
execution_mode = "buildkit"
builder = "container-builder"
```

The dependency must enable `export_cache`. Its cache directory is an explicit
input to the consuming action, so it is available in the isolated execution
workspace. Once stages the build definition with the effective ignore rules
and a final exclusion for runtime directories, so `COPY .` cannot include
imported caches. BuildKit decides which layers match the consuming Dockerfile.
Layer exports include intermediate stages, but do not include the mutable
contents of Dockerfile `RUN --mount=type=cache` mounts.

Use `cache_from` and `cache_to` lists of registry image references to share
layers between independent builders, for example
`cache_from = ["registry.example.com/service:build-cache"]`. Exports use
`mode=max` to retain intermediate stages. Registry publication requires the
builder's registry credentials. `cache_to` cannot be combined with
`cacheable = true` because a Once cache hit would skip publication.

Select an existing [remote BuildKit builder](https://docs.docker.com/build/builders/drivers/remote/)
with `builder` to execute Dockerfile instructions remotely. The Buildx client
transfers declared context inputs to that worker and returns the archive,
metadata, and optional layer cache to Once. Remote builder provisioning,
authentication, and worker resource limits are managed outside the target.
Once budgets the local client process; it does not control the remote worker's
memory budget.

## Lint Target Kinds

A lint target declares the reserved `once_lint_info` provider and generic
lint capability:

```python
providers = ["once_lint_info"]
capabilities = [capability("lint", ["default", "lint_results"])]
```

Its `lint_info` record names the analyzer, requested source scope, command,
portable report path, normalized result path, native artifacts, logs, and
execution policy. The report uses
[Static Analysis Results Interchange Format](https://docs.oasis-open.org/sarif/sarif/v2.1.0/sarif-v2.1.0.html).
Tool-specific invocation and native configuration remain in the target kind.
The Rust execution and result layers stay ecosystem-neutral.

`once query module-contract --format json` returns a complete `lint_starter`,
matching target and adapter starters, and a normalized result example. See
[Custom lint target kinds](/reference/modules/linting) for a complete direct
reporter, native report conversion, validation, execution, and cache
verification workflow.

## Script-backed Test Target Kinds

`once query module-contract --format json` returns a complete `test_starter`
for a project-local script-backed test kind, a matching
`test_target_starter`, and a `normalized_test_result_example`. The starter
resolves its host tool, invokes a package-relative adapter directly, declares
all inputs and outputs, and passes exact unit filters from
`ctx["test"]["filters"]`.

A test target declares the `once_test_info` provider and the generic test
capability:

```python
providers = ["once_test_info"]
capabilities = [capability("test", ["default", "test_results", "logs"])]
```

Its implementation returns `test_info` with this shape:

```python
{
    "schema": "once.test_info.v1",
    "target": ctx["label"]["id"],
    "runner": {
        "type": "scripted",
        "display_name": "Script-backed test",
        "metadata": {},
    },
    "command": {"argv": argv, "env": {}, "cwd": "."},
    "outputs": {
        "results": results,
        "logs": [log],
        "native_results": [],
        "coverage": [],
    },
    "listing": {"supported": True, "strategy": "normalized_results"},
    "filtering": {"case_filtering": "runner_args"},
    "sharding": {"supported": True, "granularity": "file"},
    "retries": {"supported": False, "default_attempts": 1},
    "execution": {
        "cacheable": True,
        "timeout_ms": None,
        "run_from_workspace_root": True,
    },
    "labels": [],
    "metadata": {},
}
```

The provider may also return `test_discovery_inputs`, a list of
workspace-relative files whose contents can change discovered unit
identifiers. Once fingerprints those files before reusing a manifest. When the
field is absent, Once conservatively fingerprints the provider's complete
`affected_inputs` list.

The adapter writes [JavaScript Object Notation](https://www.json.org/json-en.html)
results below the target test directory. When `ctx["test"]["batch_id"]` is
present, the adapter must add `batches/<batch_id>` to that directory before
declaring its result, log, and native-result paths. This prevents concurrent
batches of one target from overwriting each other. Once merges the batch
records into the target's canonical result after the schedule completes.

```json
{
  "schema": "once.test_results.v1",
  "target": "tests/example",
  "runner": {"type": "scripted", "metadata": {}},
  "status": "passed",
  "summary": {
    "total": 1,
    "passed": 1,
    "failed": 0,
    "skipped": 0,
    "flaky": 0
  },
  "cases": [{
    "id": "tests/example::case-name",
    "name": "case-name",
    "suite": "tests/example",
    "status": "passed",
    "attempts": [{"status": "passed"}],
    "runner_metadata": {}
  }],
  "artifacts": {"logs": ["<declared-log>"], "native_results": []}
}
```

Case identifiers are stable target-qualified semantic names. Declare
`case_filtering = "runner_args"` only when every requested identifier is
translated exactly into the native runner invocation. For a nonempty exact
request, normalized `cases` must contain exactly the requested IDs, each once,
and `summary.total` must equal the observed case count. Reporting additional
cases is not accepted as successful exact filtering. Parameterized cases need
distinct discovered IDs; setup diagnostics belong in runner metadata or
artifacts, not additional case identities. Otherwise declare `unsupported`.

Set `sharding.supported` only when exact filtering and batch-isolated outputs
are both implemented. `granularity = "file"` groups all discovered cases with
the same `file` value into one batch. `granularity = "case"` creates one batch
per stable unit. `granularity = "target"`, an absent manifest, or a stale
manifest keeps the whole target as one batch. Batch identity depends on the
target and semantic unit identifiers, not the worker count.

A failed or incomplete runner exits unsuccessfully and writes a
matching failed result when possible. A successful host process status must
never turn a runner crash or missing successful terminal record into a pass.
Once validates this complete record before it derives discovery data or marks
a scheduled batch as successful. A malformed runner, summary, attempt, or
artifact record fails an exact batch instead of being accepted as partial
evidence. A successful exact batch also requires a completed execution record,
a passing result status, no reported failing cases, and a batch-owned result
path. For exact requests, use the shared `_test_output_dir(ctx)` helper for
results and their companion artifacts. It selects
`<build_dir>/test/batches/<batch_id>` when `ctx["test"]["batch_id"]` is set and
`<build_dir>/test` otherwise. Explicit unit requests require this isolation even
when automatic sharding is disabled. A nonzero process exit always fails
regardless of existing result files.
Whole-target runners may omit normalized results, but they cannot advertise
exact execution without them.

A complete-target plan must cover every unit in the current manifest with
non-overlapping batches. Explicit unit and batch requests intentionally cover
only their requested scope and never replace complete discovery. The adapter
owns inventory completeness: declared discovery inputs must include every
input that changes test registration, and failed or incomplete collection must
not be reported as a successful complete run.

## Authoring Target Kinds

Keep ecosystem behavior within its target kind. A target kind can understand a
compiler, package manager, binary format, or platform naming convention while
still exposing the same target schema, action, and provider contracts as every
other kind.

Provider records are the cross-target kind contract. Prefer small, typed,
documented fields such as output paths, transitive inputs, flags, and
metadata. Avoid making consumers branch on a dependency target kind.

Use `toolchain_identity` for tool versions, resolved compiler paths, platform
tool selection, generated file contents, and other non-source inputs that
should invalidate cached actions. Do not log secrets or place secrets in
arguments, provider records, or action metadata.

When adding a public target kind, make it discoverable through `once query
target-kinds` and `once query schema`, provide at least one starter example,
and keep the schema, docs, and examples in sync.
