← 1.0.0 scope SAPE-8

Cost envelope: which collections, which indexes, what row ceiling, which fields escape

Status
To do
Component
server
Priority
High — an operator cannot consent to what they cannot read
Blocked by
Blocks
SAPE-11

Description

Make an operation's cost envelope readable before it is trusted, as four named facts: the collections it touches, the indexes it uses, the maximum number of rows it can return, and which fields leave the database in its result. The envelope is derived from the declaration, not written by hand alongside it, so it cannot disagree with the operation it describes.

Why now

This is the decision surface for everything else in the external-operations group: installing an operation is a consent, and consent to an unreadable thing is not consent. It belongs to 1.0.0 rather than after it because the envelope is a public shape — SAPE-11 lists it and SAPE-12 documents it, and changing the four facts after strangers have read them is a compatibility break on a surface that was published.

Measured 2026-09-20. One of the four facts already exists inside the engine: internal/store/compose.go:99 has func (s *Store) ceiling(operation Operation, within *costs) (int, error), and the comment above it at compose.go:43-50 proves by induction over depth that a composed operation's ceiling is the limit it declares, with a test named in the text. But ceiling is lowercase — it is not reachable from outside internal/store, and nothing surfaces it. The other three facts — collections, indexes, escaping fields — have no equivalent anywhere: Catalogue at internal/store/explore.go:195 is two slices, Collections []Spec and Operations []Operation, and neither carries a derived cost.

pipelines/tasks/0071 measured this same gap from the external-operations side and reaches the identical conclusion: for KindDeclaration external operations (the recommended v1 shape — a signed bundle of declarations, no third-party code), three of the four facts are already enforced at declaration time, by code that existsOperation.Collection is mandatory and checked by validateOperation; Operation.Index/From/To narrow a scan and nothing else can smuggle one in; and the row ceiling is exactly the ceiling() this ticket is about, already coerced at declaration time by N5 (internal/store/ops.go:788-798). The fourth fact, escaping fields, has no equivalent today for any operation, declared or external — 0071 calls this "a real gap, and it has been there since before external operations." Exporting Ceiling(name string, version int) (int, error) and folding it into WhatIsHere is 0071's proposed shape for criterion 1 below, and it is a re-export of a function that already exists and is already tested — not new arithmetic.

Acceptance criteria

  1. For a declared operation, the envelope can be read without running it, and reports all four facts.
  2. The row ceiling reported equals what the operation actually returns at most. Measured by declaring an operation over a collection with more rows than its limit, reading the ceiling, running it, and comparing — the two numbers match.
  3. For a composed operation, the reported ceiling is the one compose.go already proves, checked at depth greater than one. An operation whose steps call other operations reports the sum, not the top-level limit.
  4. The collection list is exact in both directions: every collection the operation reads is listed, and no collection it does not read is listed. Measured by an operation touching two of four collections.
  5. The escaping-field list is exact in the same way. An operation projecting three fields of a ten-field document reports three. If a field reaches the result through a composed step, it is reported.
  6. Changing an operation's declaration changes its envelope. A test that redeclares with a different limit and asserts the envelope moved — so the envelope cannot be a cached value that stopped tracking.

Out of scope