ADR-077 — Path C as default for parameterized modules¶
Status · Accepted Date · 2026-05-05 TA anchor ·/components/synthesizer ·/constraints/opaque-hex-blobs Related RFC · RFC-021 (closes); supersedes part of ADR-008 Related ADRs · ADR-008 (opaque-blob default; partially superseded), ADR-073 (Path C as authoring technique; this ADR extends it to apply time)
Context¶
ADR-008 made op_params and blendop_params opaque blobs by default, with Path C (programmatic decode/edit/re-encode) as a rare exception for high-value modules. RFC-012 / ADR-073 confirmed Path C is feasible and shipped it as an authoring-time technique (programmatically generate .dtstyle files at vocabulary-build time).
Operating on the discrete-vocabulary assumption produced a structural thinness: the vocabulary today has 4 hardcoded exposure entries (expo_+0.3/-0.3/+0.5/-0.5), 3 vignette intensities, 2 WB strengths (only subtle), and so on. Asking a photographer to pick +0.7 EV cannot be answered. Combinatorially enumerating every plausible magnitude is infeasible.
RFC-021 deliberated the architectural shift: extend Path C from authoring time to apply time, so the engine can synthesize an arbitrary-magnitude application of a parameterizable module without per-strength vocabulary entries.
Decision¶
Path C is the default for vocabulary modules whose photographic axis is continuous magnitude and that explicitly declare a parameters block in their manifest entry. ADR-008's opacity policy continues to govern every other module (the vast majority of darktable's ~50 photographically-meaningful modules and the blendop_params blob universally).
A parameterized module ships with: (a) a manifest entry declaring its parameters with byte-level field offsets per ADR-078, (b) a Path C decoder/encoder pair in chemigram.core.parameterize.<module_name>, and © lab-grade test coverage per ADR-080. The synthesizer at apply time decodes the entry's op_params, applies the user-supplied parameter values, re-encodes, and proceeds with the existing apply pipeline (including drawn-mask binding per ADR-076).
Modules without a parameters block continue to behave as ADR-008 specifies — opaque blobs, copied verbatim. The two paths coexist; they don't conflict.
Rationale¶
- Escapes the discrete-vocabulary trap. Continuous magnitude becomes a first-class parameter; no combinatorial vocabulary explosion.
- ADR-008's opacity policy still earns its keep for the modules where parameterization isn't worth the per-module decoder investment. The choice is now per-module instead of repo-wide.
- Composes cleanly with ADR-076 mask binding. Parameterization edits
op_params; mask binding editsblendop_params+ injectsmasks_history. They don't touch the same bytes. - Path C decoders are a known cost. RFC-012 / ADR-073 demonstrated the engineering shape; reusing those decoders at apply time is a small extension, not a new investment.
- Modversion drift handled per-decoder. Each Path C decoder is modversion-pinned and refuses to operate on mismatched blobs. Adding a new modversion is a clear failure with a clear fix; no silent corruption.
Alternatives considered¶
- Keep ADR-008's framing unchanged; author more discrete strengths instead. Rejected — combinatorially infeasible; documented in RFC-021 §Alternative A.
- Parameterize at session/agent layer instead of engine. Rejected — pushes the burden onto every integration; doesn't compose with masking; documented in RFC-021 §Alternative B.
- Use
darktable-cli --stylefor parameter overrides. Rejected — ADR-011 already rejected--stylefor vocabulary application; doesn't expose per-parameter overrides; documented in RFC-021 §Alternative C.
Consequences¶
Positive:
- Continuous magnitude is supported for any parameterized module
- Vocabulary entries collapse: 4 exposure entries → 1, 3 vignette → 1, etc. Manifest weight freed for genuinely new photographic moves.
- Agent reasoning over magnitude is direct (
apply_primitive(name="exposure", value=0.7)) rather than producing stacking workarounds. - No combinatorial vocabulary explosion required to cover the gap.
Negative:
- Per-module engineering cost for each Path C decoder (~half-day per simple single-axis module after architecture lands).
- Modversion drift risk for parameterized modules; mitigated by pinning + clear failure on mismatch.
- Manifest schema is more complex for parameterized entries (mitigated: optional; flat schema unchanged for non-parameterized entries).
Implementation notes¶
chemigram.core.parameterize.<module_name> is the namespace for Path C decoders. Each module gets its own submodule with at minimum two functions: decode(op_params: str) -> ModuleParams and encode(params: ModuleParams) -> str. Round-trip equivalence (encode(decode(blob)) == blob) is the unit-test contract.
The synthesizer's apply path checks entry.parameters (per ADR-078). When present and the caller supplied values, the decoder is invoked; the resulting op_params replaces the entry's stored op_params for that one apply call. The original .dtstyle is not mutated.
ADR-008's "Path C is the rare exception" wording is partially superseded by this ADR. ADR-008 continues to apply to blendop_params universally, to non-parameterized modules, and as the documented baseline. This ADR adds: "for modules with a manifest parameters block, Path C runs by default at apply time."