# Bindings

> Bare names and the trust-evaluation moment. Project bindings, global bindings, and how resolution works.

Esker datasets are addressed by `<owner>/<name>@<version>`. Two different owners can publish the same `<name>` — the platform never picks a winner. So how does your code stay readable when every dataset has an owner prefix?

The same way every package manager solves the same problem: bind once, then live in bare-name space.

## The moving parts

Three files. All TOML. Same shape as `package.json` + `package-lock.json` or `pyproject.toml` + `uv.lock`.

**Project bindings — `pyproject.toml`:**

```toml
[tool.esker]
owner = "statcan"   # default publish owner

[tool.esker.datasets]
"ca.corporations.registry" = "statcan/ca.corporations.registry"
"us.sec.companies"          = "sec-foundation/us.sec.companies"
```

**Lockfile — `esker.lock` (committed):**

```toml
# generated; do not edit

[[dataset]]
name           = "ca.corporations.registry"
owner          = "statcan"
version        = "1.0.0"
schema_version = "1.0.0"
content_hash   = "sha256:..."
lineage_hash   = "sha256:..."
resolved_at    = "2026-04-26T12:00:00+00:00"
```

**Global bindings — `~/.esker/config.toml` (off-project fallback):**

```toml
[datasets]
"ca.corporations.registry" = "statcan/ca.corporations.registry"
```

## The resolution rule

When you write `esker.get("ca.corporations.registry")` or `esker pull ca.corporations.registry`:

1. If the input is a full ref (`<owner>/<name>[@<version>]`), use it directly.
2. Otherwise look up the bare name in project `[tool.esker.datasets]`.
3. Otherwise look up in global `[datasets]`.
4. Otherwise: `no binding for <name> · run 'esker add <owner>/<name>' or use a full ref`.

When a project binding resolves, the lockfile (if present) pins the version. Bare names never auto-resolve when a project has _no_ binding, even if only one publisher exists. Strictness is a feature: behavior should not change the day a second publisher arrives.

## Pinned bindings

A binding can include `@<version>` to pin to an exact version:

```toml
[tool.esker.datasets]
# tracks latest — esker.lock supplies the pin
"us.treasury.yields" = "archie/us.treasury.yields"

# exact pin — durable across `esker upgrade`
"us.treasury.yields" = "archie/us.treasury.yields@1.0.0"
```

A pinned binding short-circuits the lockfile entirely — `pyproject.toml` is durable intent. `esker upgrade` refuses to bump pinned bindings.

:::note
Use a pinned binding when you want a specific version regardless of what's published later (compliance, reproducible analysis). Use an unpinned binding plus `esker.lock` when you want auto-tracking with explicit pins.
:::

## The workflow

Bind a dependency. The trust-evaluation moment.

```
$ esker search ca.corporations.registry
  statcan/ca.corporations.registry         12,847 records · 4.2k pulls/30d · verified
  community/ca.corporations.registry          8,231 records · 142 pulls/30d

$ esker add statcan/ca.corporations.registry
  ca.corporations.registry → statcan/ca.corporations.registry@1.0.0
  pyproject.toml · esker.lock
```

Reconcile the cache to the lockfile (mirror of `bun install` or `uv sync`):

```
$ esker sync
  ca.corporations.registry · statcan/ca.corporations.registry@1.0.0
```

Re-resolve to the latest hub version, rewriting the lockfile:

```
$ esker upgrade ca.corporations.registry
  ca.corporations.registry · 1.0.0 → 1.1.0
```

Drop the binding entirely:

```
$ esker remove ca.corporations.registry
  removed ca.corporations.registry
```

## Project root walk

Project bindings are scoped to the nearest `pyproject.toml`. The resolver walks up from the current working directory until it finds one. Inside a workspace this picks up the inner project's bindings, not the workspace root's.

If there is no `pyproject.toml` in the cwd or its parents, `esker add` errors:

```
$ esker add statcan/ca.corporations.registry
  no pyproject.toml in cwd or parents · use --global to bind
```

## Surgical TOML edits

`add` and `remove` use line-pattern regex on the binding lines, not full TOML round-tripping. They preserve user formatting, comments, blank lines, and key ordering outside the touched block.

The cost: a multi-line value or unusual spacing could break the regex. Standard one-line bindings work fine.

## Why

Three reasons.

1. **The owner choice is one explicit moment, not a thousand.** `esker add` is the only place you have to think about which `statcan/ca.corporations.registry` you're trusting. Everything downstream — code, CLI, notebooks — uses the bare name.
2. **Reproducibility is a checkbox, not a discipline.** `esker.lock` pins the exact content hash. Clean clone + `esker sync` reproduces your cache deterministically.
3. **The platform stays infrastructure.** Esker doesn't decide which `ca.corporations.registry` is "the canonical one." You do, once, in your project. The disambiguation page at `esker.so/<name>` ranks publishers by mechanical signal (verification, pull volume, recency) — never by editorial judgment.

## See also

- [Lockfile](https://esker.so/docs/sdk/lockfile.md) — `esker.lock` format and lifecycle
- [Reading](https://esker.so/docs/sdk/reading.md) — `esker.get` calling resolve
- [CLI bindings](https://esker.so/docs/cli/bindings.md) — `add`, `remove`, `sync`, `upgrade`
- [Handles](https://esker.so/docs/protocol/handles.md) — what an owner is
