# catalog/operations/imagery/composite.yaml
#
# The COG catalog-search SOURCE op — the STAC/registry front door of the engine
# (folia-engine composite.rs + resolve.rs). This record previously declared a
# `raster-to-raster` interface with a required `time_series` raster input; that was
# wrong — the op takes ZERO raster inputs. It resolves a registry dataset ref to a
# distribution (chooser), searches its STAC catalog over bbox+temporal, windows the
# matching COG assets, and reduces the scene stack per pixel. Corrected 2026-07-07
# (source-model review; the source-model ADR formalizes `kind`/`resolution`).

id: imagery_composite
aliases: [composite]
name: Imagery Composite
description: >
  Read a registry imagery dataset over a bbox and time window and reduce the matching
  scenes to one composite raster. Resolution is catalog-search: registry ref ->
  distribution chooser -> STAC search -> pinned COG asset windows; the per-pixel
  reduction (default median) then collapses the scene stack. A SOURCE op: zero raster
  inputs, identity = the resolved asset set + grid (ADR-0065 D3), not the query text.
version: 2.0.0   # 1.0.0 -> 2.0.0 BREAKING (defect 57): the `census` param, defaulting to `latest`. A composite IS a temporal reduction, so a catalog shipping one item per reprocessing weighted that acquisition once per record; collapsing to one row per acquisition changes the reduced value for any duplicating catalog.
op_identity: builtin:imagery_composite@7   # @5 -> @7 (defect 57, 2026-08-16): the observation census. NOT @6 — the ADR-0076 partitioned path keys engine-internally as @6 to stay DISTINCT from this whole-layer identity, so landing on 6 would alias the two paths; the partitioned constant moved to @8 in the same commit (tests/lock_key.rs asserts they never collide). ADR-0065 D4 — bump on numerics change (@3: sdk/047 validity mask; @5: ADR-0076 FIX 2 canonical reduce order). This is the WHOLE-LAYER identity; the ADR-0076 partitioned path keys engine-internally as @6 (IMAGERY_COMPOSITE_PARTITIONED_VERSION) and never appears in this registry table. NB: the canonical (year,item_id) reduce order DID bump this — zero-byte-movement is unprovable for arbitrary inputs (f32 scenes mixing 2^40 and 2^-13 round order-dependently in f64; the 500-scene experiment only sampled bounded index-valued data), so the conservative ADR-0065 D4 rule bumps.
category: imagery
type: source-to-raster

# Source taxonomy (source-model review 2026-07-07; CI-coupled to folia-engine
# lock.rs::is_source_op / is_composite_op via tests/lock_key.rs):
#   kind: source        — resolves external assets; no DAG input edges; keys from pins.
#   resolution:         — the bytes-finding mechanism the resolve stage dispatches on.
kind: source
resolution: catalog-search

# ZERO raster inputs — the data comes from the resolved distribution, not an edge.
inputs: []

# Parametric: composites whatever concept the dataset carries. requires: omitted.
requires: {}

outputs:
  - name: composite
    type: raster
    format: cog
    dtype: float32
    # concept: omitted — output concept inherits from the dataset (parametric)
    description: The windowed scene stack reduced per pixel by the chosen method

params:
  uri:
    type: string
    description: Registry dataset ref ("@usgs/landsat/c2-l2") the resolve stage expands
  bands:
    type: array
    description: Canonical band selection (mapped to assets by the distribution)
  bbox:
    type: array
    description: WGS84 AOI window [w, s, e, n]
  temporal:
    type: array
    description: Time window [start, end] (ISO dates) for the STAC search
  method:
    type: enum
    enum: [mean, median, min, max, std, count]
    default: median
    description: Per-pixel reduction method over the scene stack
  resolution:
    type: number
    default: 30
    description: Target grid resolution in metres (canonical snap, ADR-0065 D6)
  limit:
    type: integer
    description: Max scenes the STAC search returns
  catalog:
    type: string
    description: Per-layer data-locus pin (sdk/038) — restrict the chooser to one catalog
  locus:
    type: string
    description: Per-layer data-locus pin (sdk/038) — restrict the chooser to one cloud

# ADR-0065 D3 param classes (CI-coupled to lock.rs::COMPOSITE_QUERY_PARAMS via
# tests/lock_key.rs): `query` params are search/routing terms REPLACED by the resolved
# asset set + grid in the layer key — never hashed as text. Every other param is
# identity (key material): bands, method, resolution.
  census:
    type: enum
    enum: [all, first, latest]
    default: latest
    description: >
      What to do when the catalog returns several RECORDS for one ACQUISITION. Earth
      Search ships a separate item per reprocessing (S2B_31UFU_20210427_{0,1,2}_L2A share
      a sensing time to the millisecond and differ in their decoded pixels), and before
      2.0.0 each was reduced as its own observation — so a median/mean over a duplicating
      catalog weighted that one moment once per record, with surplus weight that was noise
      rather than a duplicate.
      Acquisitions are grouped by (datetime, `grid:code`). The tile term is NOT the feature
      bbox: the WGS84 bbox is a valid-data extent that moves between processing baselines
      (~470 m across the 04-27 reprocessings, measured 2026-08-16), so it under-collapses
      exactly the duplicates it would exist to find. No grid code, no collapse, loud reason.
      `latest` (the default) keeps the record whose id sorts LAST per acquisition — a
      LEXICAL rule matching Earth Search's _0_/_1_/_2_ counter, not a processing timestamp,
      which STAC items do not carry. `first` keeps catalog return order. `all` restores the
      pre-2.0.0 one-row-per-record behavior.
      IDENTITY-bearing: it changes the value, not the search, so it is absent from the
      `query` class below.

param_classes:
  # cloud_lt (scene-expression box, 2026-08-05): the configurable eo:cloud_cover
  # threshold — a SEARCH-policy term like limit, replaced by the resolved pins, so it
  # drops from key material (absent = the historical 30, byte-identical).
  query: [uri, dataset, bbox, temporal, limit, group_by, sort, query, catalog, locus, cloud_lt]

# backends: audited 2026-08-14 (defect 50). A key means a runtime that DISPATCHES this op —
# folia-engine `dispatch_op` (products/sdk/folia-engine/src/lib.rs), a `registerOp`/OP_TABLE
# entry in packages/compute, `_BUILTIN_OP_MAP` in folia/compute.py, or a backend manifest
# (folia/backends/*/backend.yaml).
backends:
  rust-engine:
    op: imagery_composite
    function: composite::composite_op
    dispatch: products/sdk/folia-engine/src/lib.rs:1011
  js:
    function: imagery_composite
  python:
    function: geo.imagery.composite
    dispatch: folia/compute.py _BUILTIN_OP_MAP
  gee:
    function: folia.backends.gee.ops.composite
    dispatch: folia/backends/gee/backend.yaml

performance:
  reduction_class: reduce                # per plan-lock.md#placement — band-per-year out of a scene stack
  throughput:                            # measured, never asserted (sdk/007)
    # px = input samples (f32 values), matching plan.rs px = est_input_bytes/4. Measured
    # on the annual-reduce kernel this op dispatches per year (temporal_reduce_flat,
    # default method median — folia-engine composite.rs); search/fetch cost is the
    # source layer's, not this op's. Release build, 16-band NaN-holey stack.
    native_px_per_s: 88688494
    measured: { on: "darwin-arm64 Apple M1 Max", date: 2026-07-01, via: "folia-compute tests/op_throughput.rs::op_throughput" }
