ADR-023 — Vocabulary primitives are .dtstyle files + manifest entries¶
Status · Accepted Date · 2026-04-27 TA anchor ·/components/synthesizer ·/contracts/dtstyle-schema ·/contracts/vocabulary-manifest Related RFC · None (concept-package decision)
Context¶
A vocabulary primitive is the unit the agent applies — expo_+0.5, tone_lifted_shadows_subject, fuji_acros. Each primitive needs both the operational data (op_params, blendop_params, modversion — what darktable applies) and the structural metadata (which layer, which modules touched, mask reference, license, version, source, etc.).
Different file formats could hold these: a single combined JSON, two separate files, an embedded structure inside a custom format. The choice affects authoring ergonomics, parsing complexity, distribution, and how vocabulary is reviewed.
Decision¶
A vocabulary primitive consists of two files:
- The
.dtstylefile — the darktable-native XML, capturing the actual edit operations. Authored in darktable's GUI by the photographer. - A manifest entry — JSON metadata in the pack's
manifest.json. Declares layer, subtype, modules touched, mask kind, version pinning, source, license, etc.
A pack (the unit of distribution) contains many .dtstyle files plus one manifest.json listing all of them.
chemigram-vocabulary/
layers/
L1/...
L2/
neutralizing/
film_sims/
fuji_acros.dtstyle
L3/
tone_lifted_shadows_subject.dtstyle
...
manifest.json <- declares all entries with metadata
Rationale¶
.dtstyleis darktable's native format. Photographers author primitives in darktable's GUI and export to.dtstyle; we use the format darktable produces, no transformation needed.- Separation of concerns. Operational data lives in
.dtstyle(darktable's domain); metadata lives in the manifest (Chemigram's domain). Each is the right tool for its job. - Distribution is natural. A vocabulary pack is a directory of
.dtstylefiles plus a manifest — it ships as a tarball or git repo without special packaging. - Inspection is straightforward.
cat fuji_acros.dtstyleshows the XML;jq '.' manifest.jsonshows the metadata. No proprietary container.
Alternatives considered¶
- Single combined JSON per primitive (embed darktable's XML inside a JSON wrapper): rejected — couples manifest schema to darktable's evolving XML format; loses the property that
.dtstylefiles are darktable-readable in their native form. - Manifest embedded inside
.dtstyle's<description>: rejected — pollutes the description field, fragile if darktable changes XML schema, hard to validate. - Single manifest combining all primitives' operational data and metadata in one big JSON: rejected — defeats the photographer's natural workflow (export
.dtstylefrom darktable, drop in directory). Forces a transform step that adds friction.
Consequences¶
Positive: - Authoring stays close to darktable's natural workflow - Manifest is clean JSON with predictable schema - Vocabulary inspection works with standard tools - Distribution is "directory + manifest"
Negative:
- Two files per primitive (.dtstyle + manifest entry) instead of one. Mitigated: the manifest has all entries together, so it's not "two files per primitive" but "all-entries-in-one-file plus per-primitive .dtstyle."
- Schema validation must check both files plus their consistency (touches in manifest matches operations in dtstyle). Done in CI.
Implementation notes¶
src/chemigram_core/vocab.py loads packs by reading manifest.json first, then loading each entry's .dtstyle from the declared path. Vocabulary CI checks (per CONTRIBUTING.md) validate both schemas plus their consistency.