# Luxar cross-language format contract — SINGLE SOURCE OF TRUTH.
#
# This file is the authoritative descriptor of the format vocabulary that the
# Python WRITER (luxar) and the TypeScript CONSUMER (@luxar/viewer) must agree
# on. Rust holds zero format strings (it exposes one decode function per scheme
# and TS decides which to call), so it is intentionally NOT a party here.
#
# Edit THIS file, then regenerate the language projections:
#     make gen-contract          # or: hatch run gen-contract
# Generated artifacts (DO NOT EDIT BY HAND):
#     packages/luxar/src/luxar/typing_utils/_format_contract.py
#     packages/luxar-viewer/src/types/format-contract.ts
# The CI gate `hatch run check-contract` fails if either projection drifts from
# this file, so a new version / encoding / node kind is a one-file edit that
# cannot silently desync the two languages.
#
# Source of record for the values below: docs/specs/GSPLATS_ZARR_FORMAT.md.

# Scene (.luxar.zarr) format version.
#   Python: typing_utils/constants.py + typing_utils/format_version.py
#   TS:     data/format-version.ts
#
# 0.2 is the self-identifying root header: `format_version` + `format_type`
# (mirroring the gsplats header) plus the software stamp `luxar_software_version`.
# 0.1 wrote only `luxar_version` — the key survives as the LEGACY read fallback
# (`legacy_scene_version_attr` below) because published, immutable stores carry
# it (the DESI Zenodo record, every `datasets/**` store).
#
# No reserved future slots: a newer MINOR (e.g. a 0.3 store read by a 0.2
# reader) is handled by POLICY — warn and load — not by pre-declaring it here;
# see `check_format_version` in both languages. Only versions this build has
# actually read end-to-end belong in `supported`.
scene_format:
  current: "0.2"
  supported: ["0.1", "0.2"]

# Standalone gsplats (.gsplats.zarr) node-tree format version.
#   Python: gsplats/io/save_gsplats.py   TS: data/scene-loader/.../load-scene.ts
gsplats_format:
  current: "3.4"
  supported: ["3.0", "3.1", "3.2", "3.3", "3.4"]

# Root-header `format_type` attr that identifies a standalone gsplats store.
format_type_gsplats: "gsplats_zarr"

# Root-header `format_type` attr that identifies a compiled scene (scene 0.2+).
#   Python: io/compiler.py (writer), typing_utils/format_version.py (reader)
#   TS:     data/format-version.ts (dispatches scene vs gsplats on this value)
format_type_scene: "luxar_zarr"

# The scene 0.1 root-header version key. Read-only compatibility: readers fall
# back to it when `format_version` is absent; the writer no longer emits it.
legacy_scene_version_attr: "luxar_version"

# Root-header attr recording the Luxar SOFTWARE version (`luxar.__version__`)
# that wrote a scene. Provenance only — it is EXCLUDED from `content_hash` by
# both hashers (`io/_compiler/finalize/hashing.py`, `io/optimize.py` via
# `HASH_EXCLUDED_ATTRS`), so two builds compiling the same scene agree on the
# digest and a `luxar optimize` restamp never churns viewer caches.
#
# DELIBERATE ASYMMETRY with gsplats: a standalone `.gsplats.zarr` keeps its own
# `luxar_gsplats_version` key for the same fact. Renaming a 3.4 header key
# without bumping the gsplats format would make "3.4" mean two shapes, and every
# published Zenodo gsplats store carries the old key. Unify on this attr at the
# next gsplats format bump (3.5), not before.
software_version_attr: "luxar_software_version"

# Scene-graph node geometry/container types.
#   Python: typing_utils/enums.py::NodeType   TS: types/data-monitor-types.ts
node_types: ["scene", "group", "points", "lines", "gsplats", "mesh", "sound"]

# The LEAF subset of `node_types` — the first-class geometry types that carry
# element data (as opposed to the `scene` / `group` containers). This is the
# VOCABULARY: "is this node a geometry leaf?". It is what the Python writer side
# asks — does this node contribute position_bounds, is it a leaf for `luxar info`
# and LOD backfill — and every geometry type belongs here from the moment it can
# be written, whether or not the viewer can draw it yet.
#   Python: io/_compiler/{bounds.py,finalize/lod_backfill.py}, cli/info_command.py
#   TS: types/format-contract.ts::GeometryTypeName
# The generator enforces that this is non-empty, a subset of `node_types`, free
# of the container types, and duplicate-free.
geometry_types: ["points", "lines", "gsplats", "mesh"]

# The subset of `geometry_types` the VIEWER can load and draw — the types that
# have a loader, a GEOMETRY_DESCRIPTORS row, a PARTIAL_EXTEND_TOLERANCE row and
# a hidden-dim tolerance arm.
#
# This is a CAPABILITY, not a vocabulary, and the distinction is load-bearing in
# both directions. Keying the viewer's dispatch tables on `geometry_types` would
# mean a newly-authorable type silently resolves to no loader; keying the Python
# writer's leaf checks on `loader_types` would mean a writable type's bounds are
# silently skipped. So the two lists are named separately and each consumer picks
# the one that matches the question it is asking. Adding a type here is the
# deliberate "switch it on in the viewer" step, and omitting it is a COMPILE
# ERROR at `LoaderByKind`, `GEOMETRY_DESCRIPTORS`, `PARTIAL_EXTEND_TOLERANCE`
# and `computeHiddenDimTolerance` — never a silent gap.
#
#   TS: data/data-loader-types.ts::GeometryKind
# The generator enforces that this is non-empty, a subset of `geometry_types`,
# and duplicate-free.
loader_types: ["points", "lines", "gsplats", "mesh"]

# Specialized-group kinds written by the compiler.
#   Python: io/_compiler/gsplat_tree.py   TS: types/data-monitor-types.ts
node_kinds: ["lod", "partition"]

# On-disk array encoding scheme names: emitted by the Python ArrayEncoder,
# recognized by the TS ArrayDecoder. NOTE the two bit-width suffix conventions
# are NOT interchangeable: the per-channel family uses `_uN`; the scalar /
# bounded / geolog-scalar / lut / rgb families use `_uintN`.
#
# The split is KEPT DELIBERATELY, not tolerated. Both spellings are live in
# published stores -- a scan of the built demo and example stores found every
# one of them carrying at least one encoding attr, including 156 occurrences of
# the short `_uN` form -- so renaming either convention would invalidate every
# published store and every checksum-pinned Zenodo record, or else require a
# third alias vocabulary that is strictly worse than the wart. The names are
# wire format; they are not free to tidy.
#
# What the split costs is that the wrong sibling of a real name reads exactly as
# plausibly as the real one. That is a documentation hazard before it is a code
# one, and its source is identifiable: the WASM/TS decode kernels are named
# `decode_<family>_u8` -- short form, uniformly, by Rust convention -- while the
# value they read off disk may be `<family>_uint8`. An author reads the kernel,
# writes its spelling into prose, and the next author copies the prose into an
# f-string that produces a store no decoder can read.
#
# So the split is enforced rather than merely documented:
#   encoding/tests/test_encoding_vocabulary.py     no document may NAME a
#                                                  non-existent sibling
#   encoding/tests/test_emitted_names_contract.py  every name the encoder EMITS
#                                                  is here, and every quantized
#                                                  name here is reachable
#   encoding/tests/test_custom_dispatch_contract   the CUSTOM dispatch keys
#   viewer .../format-contract.test.ts             the decoder knows every name
encodings:
  - none
  - broadcasted
  - array_ref
  - lut_uint8
  - lut_uint16
  - rgb_uint8
  - rgb_uint16
  - bounded_scalar_uint8
  - bounded_scalar_uint16
  - geolog_scalar_uint8
  - geolog_scalar_uint16
  - log_scalar_uint8
  - log_scalar_uint16
  - linear_perchannel_u8
  - linear_perchannel_u16
  - log_perchannel_u8
  - log_perchannel_u16
  - signed_log_perchannel_u8
  - signed_log_perchannel_u16
  - geolog_perchannel_u8
  - geolog_perchannel_u16
  - float16
  - float32
  - uint8
  - uint16
  - uint32
  - uint64

# Canonical STRUCTURAL attribute keys on scene/gsplats nodes: the header,
# identity and tree-shape keys every reader dispatches on. Appearance keys are
# the separate `render_attr_keys` list below.
#   Python: validation/writing.py::_ALLOWED_NODE_ATTRS (structural sub-block
#           asserted ⊆ this list), io/compiler.py (root header)
#   TS:     types/zarr.ts::ZarrSceneAttrs (type-level check)
attr_keys:
  - format_version
  - format_type
  - luxar_software_version
  - type
  - content_hash
  - scene_dimensions
  - display_type
  - max_elements
  - position_bounds
  - kind
  - selector
  - default_level
  - child_index

# Canonical array names inside a gsplats node (v3.1 split the packed
# `cholesky_factors` into `_diag` + `_offdiag`; the packed name stays a read
# fallback for v3.0 stores).
array_names:
  - centers
  - amplitudes
  - cholesky_factors
  - cholesky_factors_diag
  - cholesky_factors_offdiag
  - colors
  - label_ids

# ---------------------------------------------------------------------------
# On-disk vocabularies that used to be hand-kept in two languages with a
# "keep in step" comment. Each is now one list here, projected to both sides.
# ---------------------------------------------------------------------------

# `selector` attr on a kind=lod group — the UNITS of its children's
# `coverage_fraction` thresholds. "screen-area" is what every derived ladder
# stamps (projected bbox area / viewport area); "coverage" is the legacy
# diagonal metric kept for existing stores and authored lists.
#   Python: typing_utils/constants.py::LOD_SELECTORS   TS: types/lod-group.ts
lod_selectors: ["coverage", "screen-area"]

# `blending_mode` attr values, in the viewer's panel-dropdown order.
#   Python: typing_utils/enums.py::BlendingMode (set-equal; the enum orders by
#           semantics)   TS: types/blending.ts re-exports this list
blending_modes: ["additive", "volumetric", "normal", "max", "opaque", "luminous"]

# `viewer_config.tone_mapping` values (THREE tone-mapping operators by name).
#   Python: core/viewer_config.py::VALID_TONE_MAPPINGS
#   TS:     config/sections/rendering-controls/types.ts, post-processing/tone-mapping.ts
tone_mappings: ["None", "Linear", "Reinhard", "Cineon", "ACES", "AgX", "Neutral"]

# Built-in colormap names a `colormap` attr may carry (plus the `"custom"`
# sentinel, which points at a sibling `colormap_lut` array and is NOT a name).
# The LUT bytes themselves are generated by scripts/generate_builtin_colormaps.py,
# which refuses to write a name set that differs from this list.
#   Python: colormaps/builtins.py::BUILTIN_COLORMAP_NAMES
#   TS:     rendering/colormap-data.ts::BUILTIN_COLORMAPS (keys)
builtin_colormaps:
  - green
  - magenta
  - cyan
  - red
  - blue
  - yellow
  - gray
  - orange
  - bop_blue
  - bop_orange
  - bop_purple
  - viridis
  - inferno
  - plasma
  - turbo
  - fire
  - ice
  - phase
  - RdBu
  - coolwarm

# Canonical `unit` spellings a dimension may carry on disk. The Python
# `PhysicalUnit` enum is exactly this list; `meter` is an INPUT alias that the
# validator canonicalises to `m` before writing, so it is deliberately absent.
#   Python: typing_utils/enums.py::PhysicalUnit   TS: type export only
physical_units: ["nm", "um", "mm", "cm", "m", "metre", "km", "inch", "foot", "px", "s", "au"]

# `ordering` attr on a geometry node: how its elements were spatially sorted.
# "none" is a valid on-disk value (no reordering) but not a SPATIAL method —
# the writer's `ordering_method` parameter is the narrower
# `typing_utils.aliases.SpatialOrderingMethod` (morton | hilbert), asserted to
# be a subset of this list.
#   Python: gsplats/io/save_gsplats.py, io/compiler.py
#   TS:     types/{points,gsplats,lines}.ts, data/loaders/base-types.ts
ordering_methods: ["morton", "hilbert", "none"]

# Lines-only `join` attr: the vertex-stage strategy at a degree-2 polyline joint.
#   Python: typing_utils/constants.py::LINE_JOIN_STYLES   TS: types/line-join.ts
line_join_styles: ["none", "miter"]

# `line_type` attr on a lines node (how `vertices` + `segments` are interpreted).
#   Python: core/lines.py::LineType   TS: types/lines.ts::LineType
line_types: ["segments", "polyline", "loop", "indexed"]

# `nd_transform` entry shapes, keyed by dimension NAME: an affine entry carries
# a subset of `nd_transform_affine_keys`; a categorical entry carries exactly
# `nd_transform_permutation_key`. The two are mutually exclusive.
#   Python: validation/nd_transforms.py   TS: types/zarr.ts::isPermutation
nd_transform_affine_keys: ["scale", "offset"]
nd_transform_permutation_key: "permutation"

# Keys of one entry in the root `scene_dimensions.dimensions[]` list.
#   Python: core/dimensions.py::Dimension.to_dict (+ the optional `categories`)
#   TS:     types/zarr.ts::SceneDimensionAttrs (type-level check)
dimension_attr_keys:
  - name
  - unit
  - range
  - step
  - display
  - discrete
  - cyclic
  - scale
  - spatial
  - description
  - categories

# Render / appearance attribute keys a caller may author on at least one node
# type — the writer's typo allowlist (`KNOWN_RENDER_ATTRS`): a key outside this
# list is refused at write time with a "did you mean" hint, so a misspelt
# `blending="max"` can never be persisted and silently ignored by the viewer
# (#787). Type-restricted keys stay here so the hint advertises the full
# authoring surface; per-type refusals are enforced before writing.
#   Python: validation/writing.py::KNOWN_RENDER_ATTRS (= this list)
#   TS:     data/attrs-composer.ts::ComposableAttrs (keys ⊆ this list)
render_attr_keys:
  # Compositing / appearance shared by every geometry
  - absorption
  - blending_mode
  - colormap
  - gamma
  - intensity
  - layer
  - layer_order
  - offset
  - opacity
  - visible
  # Per-element interaction templates (#1917)
  - copy
  - link
  - link_target
  # Lines-only join style (#790)
  - join
  # Mesh-only shading / material / texture knobs
  - alpha_cutoff
  - ambient
  - material
  - roughness
  - metalness
  - clearcoat
  - clearcoat_roughness
  - iridescence
  - sheen
  - sheen_color
  - transmission
  - ior
  - thickness
  - attenuation_color
  - attenuation_distance
  - dispersion
  - refract_data
  - shade_exponent
  - shininess
  - specular
  - texture_filter
  - texture_wrap
  # Mesh-only nD LOADING knob, advertised here so a typo sees it in the hint
  - slab_tolerance
