# 0195 — A declaration the host can see belongs in the snapshot

> Source: https://github.com/max-listov/stitchkit/blob/master/docs/decisions/0195-a-declaration-the-host-can-see-belongs-in-the-snapshot.md

---
# 0195 — A declaration the host can see belongs in the snapshot

**Status:** Accepted
**Date:** 2026-09-22

## Context

The surface snapshot exists so that a change to the public contract cannot reach
a consumer without a human reading the diff. `mcp.inputRequired` is such a
change: an operation that gains it starts asking a question before it runs, and
an operation that loses it stops. A host codes against both.

The snapshot did not record it. `operationFingerprint` computed the declared
rounds and used them only to detect two conflicting declarations of one
operation; `operationFrom`, which builds what is actually written to the file,
had no field for them. Measured on 0.92.0: the same operation with and without a
declared round produced byte-identical manifests. A consumer was holding the
link "declared → state key configured" with a test of their own, because our
gate did not hold it.

## Decision

An operation row carries `mcp`. It is `null` when no MCP policy is declared, a
list of `{key, message, schema}` when the rounds are fixed, and the marker
`'resolved-per-call'` when a resolver decides them.

The marker is not a convenience. A dynamic policy has no list to write down —
that is the point of it — and recording an empty list would make a tool that
resolves its questions indistinguishable from a tool that asks none. What is
fixed at declaration time is *that* it resolves, and that is what is recorded.

One function, `mcpRoundsOf`, produces this shape for both the snapshot and the
fingerprint. They answer different questions — is this change visible to review,
do these two declarations conflict — and a shape that drifted between them would
let a contract change pass one gate and fail the other.

`manifestVersion` is `3`. A committed snapshot on an older version is refused by
name, with the remedy in the message, rather than failing a schema literal with
"expected 3, received 2".

## Consequences

Every committed snapshot is regenerated once, and the first regeneration is
large: the field is added to every operation row, most of them `null`.

Declaring, removing or editing a round now moves the snapshot, which is the
whole point — and a project that previously kept its own test for the link
between a declaration and its runtime configuration can drop it.

The schema digest of a round's shape is recorded, not the schema. Widening what
a question accepts moves the digest, which is the signal review needs; the
snapshot stays a fingerprint and not a second copy of the contract.
