> ## Documentation Index
> Fetch the complete documentation index at: https://docs.getcanonic.app/llms.txt
> Use this file to discover all available pages before exploring further.

# canonic.yaml config schema

> Every top-level field in the project config file, and what it governs.

`canonic.yaml` is validated against `CanonicConfig` at load time. Unknown top-level fields are ignored rather than rejected. Only `version` and `project` are required: everything else defaults.

## Top-level fields

| Field         | Type                | Default                          | Governs                                                                                                                                               |
| ------------- | ------------------- | -------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- |
| `version`     | `int`               | n/a (required)                   | Config schema version. Currently only `1` is recognized.                                                                                              |
| `project`     | `ProjectConfig`     | n/a (required)                   | Project `name` and optional `default_connection`.                                                                                                     |
| `connections` | `list[Connection]`  | `[]`                             | Configured data sources, see [Connectors](/concepts/connectors).                                                                                      |
| `llm`         | `LLMConfig \| null` | `null`                           | LLM provider config, see [Configuring an LLM](/configuring-an-llm).                                                                                   |
| `embeddings`  | `EmbeddingConfig`   | `model: all-MiniLM-L6-v2`        | Local embedding model for knowledge vector search.                                                                                                    |
| `telemetry`   | `TelemetryConfig`   | `enabled: false`                 | Opt-in telemetry, see [below](#telemetry) and [Instrumentation & evaluation](/concepts/instrumentation-and-eval#opt-in-telemetry).                    |
| `reconcile`   | `ReconcileConfig`   | propose-only                     | Auto-apply policy for ingestion, see [Ingestion & reconciliation](/concepts/ingestion-and-reconciliation#propose-only-by-default).                    |
| `feedback`    | `FeedbackConfig`    | see [below](#feedback)           | Pattern-gate and trust-cap thresholds for the E11 feedback loop, see [Feedback loop (E11)](/concepts/ingestion-and-reconciliation#feedback-loop-e11). |
| `runtime`     | `RuntimeConfig`     | `air_gapped: false`              | Air-gapped enforcement, see [LLM & embeddings runtime](/concepts/llm-runtime#air-gapped-mode--enforced-not-advisory).                                 |
| `logging`     | `LoggingConfig`     | `level: WARNING`, `format: text` | Log level, optional file, and text/json format.                                                                                                       |
| `mcp`         | `McpConfig`         | `auth: { tokens: [] }`           | Bearer-token auth for the MCP daemon's `http` transport, see [below](#mcp).                                                                           |

`version` and `project` are the only fields canonic actually needs to boot. Everything else configures a specific subsystem and can be left out entirely for a local, single-user setup: `connections` is empty until you add a data source, `llm` is `null` until you configure a provider, and the rest fall back to safe local defaults. `embeddings` and `telemetry` rarely need touching, they exist mostly for opt-in tuning and privacy control rather than day-to-day project setup.

## `connections[]`

```yaml theme={null}
connections:
  - id: warehouse_pg
    type: postgres
    params: { host: localhost, port: 5432, database: analytics, schema: analytics }
    credentials_ref: env:CANONIC_PG_PASSWORD
    read_only_role: null
```

| Field             | Type             | Default        | Governs                                                                                                                                                                                                                                                                                                                                                                                                                             |
| ----------------- | ---------------- | -------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `id`              | `str`            | n/a (required) | Unique identifier for this connection. Referenced from `semantics/<connection-id>/`, `project.default_connection`, and a dbt connection's `target_connection`.                                                                                                                                                                                                                                                                      |
| `type`            | `str`            | n/a (required) | Connector type, e.g. `postgres`, `snowflake`, `bigquery`, `sqlite`, `duckdb`, `dbt`. Must match a registered connector or the connection fails to load with `connection_error`.                                                                                                                                                                                                                                                     |
| `params`          | `dict[str, Any]` | `{}`           | Connector-specific settings such as `host`, `port`, `database`, or `schema`. What's valid here depends entirely on `type`.                                                                                                                                                                                                                                                                                                          |
| `credentials_ref` | `str \| null`    | `null`         | Where to resolve this connection's secret from, never the secret itself. See below.                                                                                                                                                                                                                                                                                                                                                 |
| `read_only_role`  | `str \| null`    | `null`         | Reserved for a future read-only enforcement mode. Accepted and stored today but not yet consulted by any connector.                                                                                                                                                                                                                                                                                                                 |
| `rls_enforced`    | `bool`           | `false`        | Operator attestation that this connection's warehouse enforces row-level security independent of canonic (per-tenant credentials, or warehouse-native RLS). canonic cannot verify this claim, only trust it. Must be `true` for `run_sql` to be served on this connection once a [tenancy policy](/concepts/tenancy-and-access-control) is active, see [the `run_sql` gate](/concepts/tenancy-and-access-control#the-run_sql-gate). |

A connection is how canonic reaches an actual data source. `id` is the name every other file uses to point back at it, `type` picks which connector implementation handles it, and `params` carries whatever that connector needs to connect, host and port for a warehouse, a manifest path for dbt. `credentials_ref` keeps the secret itself out of `canonic.yaml` entirely, instead of a password you write a pointer to where the password actually lives.

* `credentials_ref` must be a reference, one of `env:`, `keyring:`, `file:`, or `provider:`, never a literal secret. Config validation rejects a literal outright. `llm.api_key_ref` and the MCP auth refs accept the same schemes minus `provider:`, see [Dynamic credentials](#dynamic-credentials) below.
* Postgres/Redshift connections additionally recognize `schema`/`schemas` (search path) and `tables` (glob patterns narrowing introspection), plus `fetch_column_stats: true` to merge zero-scan cardinality/null-ratio stats into the returned schema (a no-op on SQLite/DuckDB).
* `dbt` connections recognize `manifest_path` (path to the compiled `manifest.json`, default `manifest.json`) and `target_connection` (the id of the physical, queryable connection whose tables this manifest describes, see [Connectors](/concepts/connectors#definition)). `target_connection` must name a connection actually declared in `connections[]`. An unknown id fails config validation at load time. Omitting it falls back to the dbt connection's own id.

```yaml theme={null}
  - id: jaffle_dbt
    type: dbt
    params: { manifest_path: dbt/manifest.json, target_connection: warehouse_pg }
```

### Dynamic credentials

`env:`, `keyring:` and `file:` all resolve once, to a fixed string that stays valid until someone rotates it by hand. That does not fit a credential the cloud provider issues with a built-in expiry. A Redshift IAM credential lives roughly 15 to 60 minutes, so a connection string built when the daemon starts is dead well before the daemon is.

`provider:<name>` covers that case. The name selects a registered credential provider, which fetches a fresh credential from the issuer and reports when it expires. Canonic caches it and refetches shortly before it runs out, so a long-lived daemon keeps working without an operator touching anything.

```yaml theme={null}
connections:
  - id: warehouse_rs
    type: redshift
    params:
      cluster_id: analytics-cluster
      db_user: canonic_ro
      region: eu-central-1
      host: analytics-cluster.abc123.eu-central-1.redshift.amazonaws.com
      dbname: analytics
    credentials_ref: provider:aws-iam-redshift
```

No secret value appears in `canonic.yaml`. `cluster_id`, `db_user` and `region` are not secrets, they tell the provider *how* to fetch a credential, the same way a `file:` path is not a secret.

| Provider           | Fetches                                                                                          | Required params                                                                                                               |
| ------------------ | ------------------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------- |
| `aws-iam-redshift` | Redshift IAM credentials, provisioned (`GetClusterCredentials`) or serverless (`GetCredentials`) | `cluster_id` + `db_user`, or `workgroup_name`. Optional: `region`, `dbname`, `duration_seconds` (default 3600), `auto_create` |

Things worth knowing before you reach for it:

* **Redshift only, today.** `provider:` refs are resolved on every connect, and Redshift is the only connector that does that so far. Pointing another connector at one fails with a clear error rather than silently freezing the first credential it ever saw.
* **Not for inbound auth.** `llm.api_key_ref`, `mcp.auth.tokens[].token_ref` and `mcp.auth.oauth.client_secret_ref` are resolved once at startup and held. A provider credential there would go stale rather than refresh, so config validation rejects it.
* **`aws-iam-redshift` needs boto3**, which canonic does not install. `pip install boto3`. Credentials for the AWS call itself come from the standard boto3 chain (instance role, `AWS_PROFILE`, environment).
* **Rejected under `air_gapped: true`.** Fetching from AWS is network egress, which is what that mode exists to prevent.
* **Nothing is written to disk.** A fetched credential lives in memory for the life of the process that fetched it. A CLI invocation and a running daemon each fetch their own.

## `llm`

```yaml theme={null}
llm:
  provider: anthropic       # openai_compatible | anthropic | openai | github_copilot
  base_url: null            # required only for openai_compatible
  model: claude-opus-4-8
  api_key_ref: env:ANTHROPIC_API_KEY
  tasks: { reconcile: claude-opus-4-8 }   # per-task overrides: draft, reconcile, extract
```

| Field         | Type             | Default        | Governs                                                                                                                                                                                  |
| ------------- | ---------------- | -------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `provider`    | `str`            | n/a (required) | `openai_compatible` \| `anthropic` \| `openai` \| `github_copilot`. Picks which credential rule below applies and how the call is routed.                                                |
| `base_url`    | `str \| null`    | `null`         | Endpoint to call. Required for `openai_compatible`, since there's no default host to fall back to. Optional for the hosted providers, which otherwise reach their own default endpoint.  |
| `model`       | `str`            | n/a (required) | Model identifier passed straight through to the provider, e.g. `claude-opus-4-8`.                                                                                                        |
| `api_key_ref` | `str \| null`    | `null`         | Reference to the API key, never the key itself. Required for `openai`/`anthropic`, forbidden for `github_copilot` since that provider authenticates itself outside `canonic.yaml`.       |
| `tasks`       | `dict[str, str]` | `{}`           | Per-task model overrides keyed by task name (`draft`, `reconcile`, `extract`), letting you route a cheaper or more capable model to a specific step instead of using `model` everywhere. |

Each provider has a fixed credential requirement, enforced at load. `openai_compatible` requires `base_url`, `openai` and `anthropic` require `api_key_ref`, and `github_copilot` forbids one entirely since it authenticates through a device-code flow that never touches `canonic.yaml`. Getting this wrong fails config validation immediately rather than surfacing as a runtime error the first time canonic tries to call the model.

## `reconcile`

```yaml theme={null}
reconcile:
  auto_apply:
    enabled: false             # default: propose-only
    min_confidence: 0.95
    max_provenance: inferred   # never auto-apply over human_curated+
    never: [grain, joins, measures]
  strict_contradictions: false
```

| Field                       | Type             | Default                    | Governs                                                                                                                                                                                                                                           |
| --------------------------- | ---------------- | -------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `auto_apply.enabled`        | `bool`           | `false`                    | Whether `canonic ingest` may apply a proposal automatically instead of leaving it for review. Off by default, ingestion is propose-only until you opt in.                                                                                         |
| `auto_apply.min_confidence` | `float` (0 to 1) | `0.95`                     | The minimum confidence score a proposal needs before it's eligible for auto-apply. Anything below this threshold is always left for manual review, regardless of `enabled`.                                                                       |
| `auto_apply.max_provenance` | `Provenance`     | `inferred`                 | The highest existing provenance tier auto-apply is allowed to overwrite. Since it's capped at `inferred`, a fact already marked `human_curated` or `board_approved` can never be silently rewritten, no matter how confident the new proposal is. |
| `auto_apply.never`          | `list[str]`      | `[grain, joins, measures]` | Field categories that always require human review, even if they'd otherwise pass the confidence and provenance checks. These are the structural fields most likely to break a query silently if an automated proposal gets them wrong.            |
| `strict_contradictions`     | `bool`           | `false`                    | When `true`, a run that surfaces any flagged contradiction fails with exit code 14 instead of just recording it. Meant for CI, where you want ingestion to gate on data-model disagreements rather than pass through them.                        |

`reconcile` only governs how eagerly `canonic ingest` is allowed to write changes back to `semantics/`. It has no effect on what evidence gets collected, only on whether a resulting proposal can be applied without a human looking at it first. See [Ingestion & reconciliation](/concepts/ingestion-and-reconciliation#propose-only-by-default) for how proposals are generated in the first place.

## `feedback`

```yaml theme={null}
feedback:
  pattern_min_count: 2        # distinct-enough wrong_definition outcomes before evidence is minted
  pattern_window_days: 90     # window the count is evaluated over
  pattern_min_markers: 1      # distinct markers (people/agents) required within that window
  trust_cap_window_days: 90   # how long a confirmed wrong_definition caps the trust tier at caution
```

| Field                   | Type           | Default | Governs                                                                                                                                        |
| ----------------------- | -------------- | ------- | ---------------------------------------------------------------------------------------------------------------------------------------------- |
| `pattern_min_count`     | `int` (`ge=1`) | `2`     | How many distinct-enough `wrong_definition` outcomes on a binding are required before `canonic ingest` mints E4 contradiction evidence.        |
| `pattern_window_days`   | `int` (`ge=1`) | `90`    | The rolling window `pattern_min_count` is evaluated over.                                                                                      |
| `pattern_min_markers`   | `int` (`ge=1`) | `1`     | How many distinct `marked_by` roles must be represented within the window for the pattern to count (guards against one noisy marker).          |
| `trust_cap_window_days` | `int` (`ge=1`) | `90`    | How long a recent confirmed `wrong_definition` outcome caps the binding's E14 trust tier at `caution`, independent of the evidence gate above. |

See [Feedback loop (E11)](/concepts/ingestion-and-reconciliation#feedback-loop-e11) for how these thresholds are used.

## `telemetry`

```yaml theme={null}
telemetry:
  enabled: false
  endpoint: null                    # e.g. https://collector.example.com/ingest
  transport_acknowledged: false
  auth_token_ref: null              # e.g. env:CANONIC_TELEMETRY_TOKEN
```

| Field                    | Type          | Default | Governs                                                                                                                                                                                                                                                     |
| ------------------------ | ------------- | ------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `enabled`                | `bool`        | `false` | Opt-in flag. Forced to `false` under `runtime.air_gapped: true` (load-time rejection, not just a no-op).                                                                                                                                                    |
| `endpoint`               | `str \| null` | `null`  | Destination URL for `canonic audit --telemetry-send`. Must start with `http://` or `https://` when set. No default, canonic never guesses a collector.                                                                                                      |
| `transport_acknowledged` | `bool`        | `false` | Attestation that this project has reviewed the exact aggregate payload (`canonic audit --telemetry-preview`) before allowing a real send. canonic cannot verify that a review happened. This is a project-level policy decision recorded in `canonic.yaml`. |
| `auth_token_ref`         | `str \| null` | `null`  | Optional bearer token for `endpoint`, resolved the same way as `credentials_ref` (`env:`/`keyring:`/`file:`, never a literal).                                                                                                                              |

`canonic audit --telemetry-send` only sends when **all** of `enabled`, `endpoint`, and `transport_acknowledged` are set, and `runtime.air_gapped` is `false`. Missing any one of them fails closed with a structured error (`telemetry_not_configured`). Nothing is ever sent implicitly. See [Instrumentation & evaluation](/concepts/instrumentation-and-eval#opt-in-telemetry) for the full payload shape and content-safety guarantees.

## `runtime`

```yaml theme={null}
runtime:
  air_gapped: false
  allow_cidrs: []   # explicit private/LAN ranges for an on-prem inference host
```

| Field         | Type        | Default | Governs                                                                                                                                                                                                                                                                                         |
| ------------- | ----------- | ------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `air_gapped`  | `bool`      | `false` | Turns on the enforced privacy guarantee: every model endpoint must resolve locally or to an allowlisted range, and no context may leave the machine. This is checked at load time, not just documented behavior, so a misconfigured project refuses to start rather than silently phoning home. |
| `allow_cidrs` | `list[str]` | `[]`    | Additional private/LAN CIDR ranges to trust under `air_gapped`, for a setup where inference runs on a separate on-prem host rather than the local machine. Only consulted when `air_gapped` is `true`. By default, air-gapped mode trusts localhost only.                                       |

When `air_gapped: true`, load-time validation additionally rejects a public `llm.base_url`, `telemetry.enabled: true`, and any `credentials_ref`/`api_key_ref` using a non-local scheme. The check runs once, at config load, so there's no window where a project starts up in a state that could leak context off the machine. Telemetry has no allowlist path the way `llm.base_url` does via `allow_cidrs`. Under `air_gapped`, it is hard off regardless of `telemetry.endpoint`/`transport_acknowledged`.

## `mcp`

```yaml theme={null}
mcp:
  auth:
    tokens:
      - client_id: alice
        token_ref: env:CANONIC_MCP_TOKEN_ALICE
      - client_id: bob
        token_ref: env:CANONIC_MCP_TOKEN_BOB
      - client_id: merchant-4711-agent
        token_ref: env:CANONIC_MCP_TOKEN_4711
        claims: { merchant_id: '4711', roles: [merchant_viewer] }   # tenancy/RBAC only
    oauth:
      mode: proxy   # proxy | jwt
      issuer_url: https://idp.example.com
      client_id: canonic-mcp
      client_secret_ref: env:CANONIC_OAUTH_CLIENT_SECRET   # proxy mode only
      scopes: [openid, profile, email]
      base_url: https://canonic.internal.example.com       # proxy mode only, the daemon's own public URL
      verify_id_token: false                                # proxy mode only, see below
      claim_mapping:                                        # tenancy/RBAC only
        merchant_id: https://example.com/merchant_id
        roles: roles
```

| Field                          | Type                     | Default        | Governs                                                                                                                                                                                                                                                                                                                           |
| ------------------------------ | ------------------------ | -------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `auth.tokens`                  | `list[McpTokenEntry]`    | `[]`           | Bearer tokens accepted by the MCP daemon's `http` transport, one entry per client.                                                                                                                                                                                                                                                |
| `auth.tokens[].client_id`      | `str`                    | n/a (required) | Name attributed to that token's caller in `.canonic/events.jsonl`, so usage can be traced per user rather than per daemon instance.                                                                                                                                                                                               |
| `auth.tokens[].token_ref`      | `str`                    | n/a (required) | Reference to the token's value, never the token itself, resolved the same way as `credentials_ref`.                                                                                                                                                                                                                               |
| `auth.tokens[].claims`         | `dict[str, Any]`         | `{}`           | Claims carried by this static token (e.g. `{merchant_id: "4711", roles: ["merchant_viewer"]}`), keyed by the raw claim name a [tenancy/role policy](/concepts/tenancy-and-access-control)'s `claim` field names. Static tokens carry claims inline since there's no IdP to ask.                                                   |
| `auth.oauth`                   | `McpOAuthConfig \| null` | `null`         | OAuth 2.1 auth for the MCP daemon's `http` transport, delegated to `fastmcp`. Independently optional from `auth.tokens`. Both configured is a supported state, not just tolerated.                                                                                                                                                |
| `auth.oauth.mode`              | `"proxy" \| "jwt"`       | n/a (required) | `proxy`: presents a DCR-compliant OAuth server to MCP clients and relays the login to the upstream IdP (Authorization Code + PKCE), the mode most SSO deployments use. `jwt`: the IdP hands the client a JWT directly and the daemon only verifies its signature against the IdP's JWKS, no proxy state.                          |
| `auth.oauth.issuer_url`        | `str`                    | n/a (required) | The IdP's base URL. In `proxy` mode, `mode: proxy` discovers endpoints from `issuer_url + /.well-known/openid-configuration`.                                                                                                                                                                                                     |
| `auth.oauth.client_id`         | `str \| null`            | `null`         | Required in `proxy` mode: the client registered with the upstream IdP. Not used in `jwt` mode.                                                                                                                                                                                                                                    |
| `auth.oauth.client_secret_ref` | `str \| null`            | `null`         | `proxy` mode only. Reference to the upstream client's secret, resolved the same way as `credentials_ref`. Rejected in `jwt` mode (no proxy to authenticate as).                                                                                                                                                                   |
| `auth.oauth.scopes`            | `list[str]`              | `[]`           | OAuth scopes requested from the IdP / required on incoming tokens.                                                                                                                                                                                                                                                                |
| `auth.oauth.base_url`          | `str \| null`            | `null`         | Required in `proxy` mode: the daemon's own public URL (where the IdP redirects back to). Rejected in `jwt` mode.                                                                                                                                                                                                                  |
| `auth.oauth.audience`          | `str \| null`            | `null`         | `jwt` mode only. Expected token audience. Recommended: without it, any token the IdP issued for *any* resource is accepted, not just this daemon.                                                                                                                                                                                 |
| `auth.oauth.jwks_uri`          | `str \| null`            | `null`         | `jwt` mode only. Skips OIDC discovery for IdPs that don't publish `/.well-known/openid-configuration`.                                                                                                                                                                                                                            |
| `auth.oauth.verify_id_token`   | `bool`                   | `false`        | `proxy` mode only. Verifies the upstream OIDC *id\_token* instead of the *access\_token*. Rejected in `jwt` mode. See below.                                                                                                                                                                                                      |
| `auth.oauth.claim_mapping`     | `dict[str, str]`         | `{}`           | Maps a [tenancy/role policy](/concepts/tenancy-and-access-control)'s raw `claim` name (e.g. `merchant_id`) to the namespaced claim key the IdP actually issues (e.g. `https://example.com/merchant_id`), which most IdPs require for custom claims. A `claim` absent from this mapping is looked up under its own name unchanged. |

This block only matters for `canonic mcp start --transport http`. The default `stdio` transport is a local subprocess and needs no auth, process-level trust is enough. `http` transport is network-reachable, though, so it refuses to start unless at least one mechanism resolves: a token here (or via the `--token-ref` CLI override) and/or `auth.oauth`. When both are configured, a request is checked against the static token map first (no network call), falling through to OAuth verification if no static token matches: the two stay independently revocable, a token entry by editing `canonic.yaml`, an OAuth-issued token at the IdP. `canonic mcp status` reports which mechanism(s) are active on a running daemon. See [Connecting your agent](/mcp-integration/connecting-your-agent#remote-enterprise-deployment) for the full remote-deployment walkthrough.

<Note>
  `auth.oauth.verify_id_token` matters more than its default suggests. In `proxy` mode, `OIDCProxy` verifies the upstream IdP's *access token* by default, but many IdPs (Google, GitHub, some Okta setups) issue that as an opaque, non-JWT string, which fails verification outright rather than just producing a poor client identity. Setting `verify_id_token: true` verifies the *id\_token* instead, which the OIDC spec guarantees is always a standard JWT carrying `sub`/`email` claims. This also controls what ends up as `client_id` in `.canonic/events.jsonl`: with it off, `client_id` comes from the access token's `client_id`/`azp`/`sub` claim (often an opaque subject id). With it on, from the id\_token's, which is far more likely to be a meaningful identity like an email address.
</Note>

## Reserved directories

Independent of `canonic.yaml` fields, every project scaffolds four committed context directories plus a git-ignored `.canonic/` local-state directory (created with `0o700` permissions):

| Directory      | Contents                                                                           | Schema reference                                     |
| -------------- | ---------------------------------------------------------------------------------- | ---------------------------------------------------- |
| `semantics/`   | Hand-drafted-then-reviewed relations: grain, columns, measures, dimensions, joins. | [Semantic source schema](/reference/semantic-schema) |
| `knowledge/`   | Markdown pages with YAML frontmatter carrying business meaning.                    | [Knowledge page schema](/reference/knowledge-schema) |
| `contracts/`   | Human-owned metric bindings, guardrails, finality rules, and assertions.           | [Contract schema](/reference/contract-schema)        |
| `raw-sources/` | System-generated, committed audit trail, never hand-edited.                        | See below.                                           |

`contracts/policies/` (`tenancy.yaml` / `roles.yaml`) is optional. Its presence is the feature switch for [tenancy & access control](/concepts/tenancy-and-access-control), and a project without it behaves exactly as it did before that layer existed.

### `raw-sources/<connection-id>/evidence.jsonl`

Not a hand-authored config file: it's a deterministically-sorted, one-JSON-object-per-line snapshot of the evidence ingestion collected for a connection, written by `DiskSnapshotStore` so re-running ingestion against the same source yields a byte-identical file. Each line validates against `EvidenceItem`:

| Field                | Type              | Governs                                                                                                                                                                          |
| -------------------- | ----------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `source`             | `str`             | The connection id this evidence came from.                                                                                                                                       |
| `kind`               | `str` (open set)  | `relation_schema` \| `observed_query` \| `definition` \| `doc_evidence` \| `usage_evidence` \| `answer_outcome`, or an unrecognized future kind (accepted, just not dispatched). |
| `acquisition_tier`   | `AcquisitionTier` | Curation rank of the evidence, e.g. `live`, `modeling`, `query_history`, `declarative`, `sample`, `hand_authored`.                                                               |
| `payload`            | `dict[str, Any]`  | Shape depends on `kind`, e.g. a `RelationSchema` for `relation_schema`.                                                                                                          |
| `source_fingerprint` | `str`             | sha256 identifying the exact evidence content, for dedup and drift detection.                                                                                                    |
| `observed_at`        | `datetime`        | When the evidence was collected.                                                                                                                                                 |

This is what [`canonic ingest`](/cli-reference/ingest) reads to draft proposals against `semantics/`. You'd only ever read it, not write it.
