Skip to content

TA — Technical Architecture

Reference document for the tech plane. Version · v0.1 · 2026-04-27 Sources · docs/concept/04-architecture.md, examples/phase-0-notebook.md

The reference document for everything technical. Per-artifact docs (RFCs, ADRs) anchor into this with paths like TA/components/synthesizer or TA/constraints.

This document is read by linking-into specific sections — never end-to-end. It's amended whenever state changes (new component, new constraint, RFC closes into ADR).

If TA drifts toward narrative ("this is how we got here"), it has stopped being a reference. The narrative belongs in the concept package (the architecture doc).


components

The five subsystems Chemigram's engine decomposes into. Boundaries are stable; per-component design lives in linked PRD/RFC/ADR.

components/synthesizer

XMP composition engine. Reads .dtstyle files, parses XMPs, synthesizes new XMPs by composing vocabulary entries onto a baseline. Pure file operations — no rendering, no AI.

Files (shipped): src/chemigram/core/xmp.py, src/chemigram/core/dtstyle.py (Slice 1)

Public API: - parse_dtstyle(path) → list[PluginEntry] - parse_xmp(path) → XMP - synthesize_xmp(baseline_xmp, vocabulary_entries) → XMP - write_xmp(xmp, path) → None

Anchored from: RFC-001, ADR-002, ADR-008, ADR-009, ADR-010

components/render-pipeline

Sequence of stages producing a JPEG from an XMP. v1 has one stage; the abstraction admits N.

Files (shipped): src/chemigram/core/pipeline.py, src/chemigram/core/stages/__init__.py, src/chemigram/core/stages/darktable_cli.py (Slice 1)

Public API: - class PipelineStage(Protocol) — inputs/outputs/run contract - class DarktableCliStage(PipelineStage) — the v1 stage - Pipeline(stages: list[PipelineStage]).run(context) → StageResult

Anchored from: RFC-005, ADR-004, ADR-005, ADR-013

components/versioning

Per-image content-addressed DAG of XMP snapshots. "Mini git for photos."

Files (shipped): src/chemigram/core/versioning/{canonical,repo,ops,masks}.py (v0.2.0)

Public API: - canonical_bytes(xmp) → bytes, xmp_hash(xmp) → str — content-address primitive - ImageRepo.init(root), write_object, read_object, write_ref, resolve_ref, list_refs, delete_ref, append_log, read_log - snapshot(repo, xmp, *, label?, parent="HEAD", metadata?) → hash - checkout(repo, ref_or_hash) → Xmp - branch(repo, name, from_="HEAD") → ref_name - log(repo, *, ref?, limit?) → list[LogEntry] - diff(repo, hash_a, hash_b) → list[PrimitiveDiff] - tag(repo, name, hash_?) → ref_name

Anchored from: RFC-002 (→ ADR-054), ADR-017, ADR-018, ADR-019, ADR-020. (Mask-registry surface — RFC-003 / ADR-055 / ADR-021 / ADR-022 — removed in v1.5.0 per ADR-076; the PNG path was a silent no-op.)

components/masking

Mask serialization for vocabulary entries and apply-time mask construction. Four mask sources, all serializing to bytes darktable's mask system consumes per ADR-076: drawn-form geometry (gradient / ellipse / rectangle / path encoded into <darktable:masks_history>), parametric range filters (luminance + HSL color ranges via blendif byte fields in blendop_params), retouch heal/clone forms, and LLM-vision-derived polygon masks (constructed by the chat-client's vision-capable LLM, materialized as path-form geometry).

Files (shipped v1.4.0, generalized v1.9.0): src/chemigram/core/masking/dt_serialize.py — encoders for dt_masks_point_gradient_t / dt_masks_point_ellipse_t / N-vertex dt_masks_point_path_t / circle (retouch) / clone-source mask_src; blendop_params patching for drawn + parametric + drawn+parametric combinations; retouch byte encoders (dt_iop_retouch_form_data_t, 13260-byte op_params blob). High-level apply helper at chemigram.core.helpers.apply_with_mask (renamed from apply_with_drawn_mask; backcompat alias preserved).

Public API: - apply_with_mask(baseline, dtstyle, mask_spec, *, opacity=100.0) → Xmp — apply a vocabulary entry's dtstyle bound to a mask. Routes drawn-only / parametric-only / drawn+parametric automatically based on mask_spec shape. Routed by apply_primitive either from the entry's manifest mask_spec or from a caller-supplied mask_spec parameter (CLI: --mask-spec '<json>', MCP: mask_spec arg). Caller override wins when both present. - apply_spot_retouch(baseline, *, kind, x, y, radius, source_x?, source_y?, opacity=100.0) → Xmp — apply a single retouch form (heal or clone) at the given coordinate. Surfaced via the apply_spot MCP tool (sister to apply_primitive; RFC-025 / ADR-087).

Schema (RFC-029 / ADR-084 + RFC-024 / ADR-085):

mask_spec: {
  "dt_form": "gradient"|"ellipse"|"rectangle"|"path",  # spatial (optional)
  "dt_params": {...},                                   # form-specific kwargs
  "range_filter": {                                      # parametric (optional)
    "kind": "luminance"|"color_h"|"color_s"|"color_l",
    "min": <0..1>, "max": <0..1>, "feather": <0..0.5>, "invert": <bool>
  }
}
At least one of dt_form or range_filter must be present. Both present = drawn AND parametric (intersection). No PNG, no separate registry — see ADR-076.

Anchored from: ADR-076 (closes the prior PNG-mask path retroactively; supersedes ADR-021/022/055/057/058/074)

components/mcp-server

Adapts subsystems 1–4 as agent-callable tools. Thin layer.

Files: src/chemigram/mcp/server.py (bootstrap + stdio), src/chemigram/mcp/registry.py (tool registry + ToolContext), src/chemigram/mcp/errors.py (ToolResult / ToolError / ErrorCode), src/chemigram/mcp/_test_harness.py (in-memory client/server harness), src/chemigram/mcp/tools/*27 tools as of v1.10.0 across vocab/edit (8: list_vocabulary, list_masks_vocabulary, apply_primitive, apply_per_region, propagate_state, wb_from_gray_card, remove_module, reset, get_state), retouch (1: apply_spot per ADR-087), versioning (6), rendering (3), ingest (3), context (6 including log_vocabulary_gap). The 5 mask tools (generate_mask/regenerate_mask/list_masks/tag_mask/invalidate_mask) shipped in v0.3.0–v0.4.0 and were removed in v1.5.0 per ADR-076. Subsequent additions: apply_spot (v1.9.0 / ADR-087), apply_per_region (v1.9.0 / RFC-031), list_masks_vocabulary (v1.9.0 / RFC-032), propagate_state + wb_from_gray_card (v1.10.0). See CHANGELOG.

Tool surface: see TA/contracts/mcp-tools.

Anchored from: ADR-006, ADR-033, ADR-056 (closes RFC-010)

components/cli

Subprocess-callable adapter over the same core that the MCP server adapts. Mirrors the MCP tool surface verb-for-verb (with _- for shell ergonomics). Ships in v1.3.0 (RFC-020 closure). Two adapters, one core; no domain logic in either adapter layer (lint-enforced).

Files (planned v1.3.0): src/chemigram/cli/main.py (Typer root app + global-options callback), src/chemigram/cli/commands/* (one file per verb group), src/chemigram/cli/output.py (HumanWriter + JsonWriter behind OutputWriter Protocol), src/chemigram/cli/exit_codes.py (ExitCode IntEnum), src/chemigram/cli/error_mapping.py (maps chemigram.mcp.errors.ErrorCode to ExitCode).

Entry point: chemigram binary via pyproject.toml [project.scripts], alongside chemigram-mcp.

Verb surface: see RFC-020 §F. Each verb maps 1:1 to an MCP tool from ADR-033/056 plus a diagnostic chemigram status (not a tool wrapper).

Output: human-readable text by default; NDJSON via --json. Schema mirrors chemigram.mcp.errors.ToolResult shape; versioned independently of package SemVer (same pattern as ADR-045 for prompts); surfaced in chemigram status.

Exit codes: ExitCode IntEnum mapped from ErrorCode; the two enums move in lockstep, audit-tested.

Constraint (ADR-071): No XML, subprocess, or raw filesystem imports in this layer or in chemigram.mcp/. All domain operations come from chemigram.core. CI lints for forbidden imports.

Anchored from: PRD-005, RFC-020, ADR-069, ADR-070, ADR-071, ADR-072

components/prompts

Versioned prompt templates loaded by the MCP server at session start (and by the eval harness for autonomous Mode B runs). Append-only, MANIFEST-driven, Jinja2-templated.

Files (shipped v0.3.0): src/chemigram/mcp/prompts/store.py (PromptStore), src/chemigram/mcp/prompts/MANIFEST.toml (active-version registry), src/chemigram/mcp/prompts/mode_a/system_v1.j2 (Mode A v1; migrated from docs/agent-prompt.md). Mode B + helpers ship in later phases per their RFC closures.

Public API: - PromptStore.render(path, context, version=None, provider=None) → str - PromptStore.active_version(path) → str - PromptStore.context_schema(path) → dict - PromptStore.list_templates() → list[str]

Anchored from: RFC-016, ADR-043, ADR-044, ADR-045

components/context

Per-image and per-photographer context loaders. Reads tastes (multi-scope per ADR-048: ~/.chemigram/tastes/_default.md plus brief-declared genre files), brief.md (with Tastes: declaration), notes.md (with line-truncation summarization for long files), recent log entries, recent vocabulary gaps. Tolerates missing files (returns empty structures so the agent's first turn works on a fresh workspace).

Files (shipped v0.5.0): src/chemigram/core/context/__init__.pyTastes, Brief, Notes, RecentLog, RecentGaps loaders + their content dataclasses. MCP wiring in src/chemigram/mcp/tools/context.py (read_context + propose/confirm tools).

Public API: loaders' .load(workspace, ...) classmethods; read_context(image_id) MCP tool.

Anchored from: RFC-011 (→ ADR-059), ADR-031, ADR-048

components/session

JSONL session transcripts per ADR-029. Header line, per-turn entries (tool_call, tool_result, proposal, confirmation, note), footer on close. Append-only, flushed per write.

Files (shipped v0.5.0): src/chemigram/core/session/__init__.pySessionTranscript, SessionHeader, start_session. MCP server's tool dispatch (build_server(transcript=...)) auto-records every tool call.

Public API: start_session(workspace, *, mode, session_id, ...) -> SessionTranscript; transcript.append_* helpers; transcript.close(summary?).

Anchored from: ADR-029, ADR-031, RFC-014 (→ ADR-061)

components/eval

Headless eval harness for autonomous Mode B. Runs the agent against versioned golden datasets, computes mechanical and semantic metrics, writes run manifests for cross-run comparison. Phase 5 build; design locked in Phase 1.

Files (planned): src/chemigram/eval/runner.py, src/chemigram/eval/scenarios.py, src/chemigram/eval/metrics/{mechanical,semantic}.py, src/chemigram/eval/reports.py, src/chemigram/eval/manifest.py

Public API (sketched): - EvalRunner(golden_version, prompt_versions, model_config).run_all() → EvalRunResult - EvalRunner.run_scenario(id) → EvalScenarioResult - EvalRunResult.save(path) → None - EvalRunResult.append_history(path) → None

Anchored from: RFC-017, ADR-046, ADR-047


contracts

Data shapes between components. The boundaries that survive across implementation iterations.

contracts/dtstyle-schema

.dtstyle files are XML conforming to darktable's style format. Calibrated to darktable 5.4.1; per 04/3.1 and verified against Phase 0 fixtures:

<darktable_style version="1.0">
  <info>
    <name>NAME</name>
    <description>...</description>
    <iop_list>...</iop_list>      <!-- optional; absent in single-module exports -->
  </info>
  <style>
    <plugin>
      <num>N</num>
      <module>MODVERSION</module>
      <operation>OPERATION_NAME</operation>
      <op_params>HEX_BLOB</op_params>
      <enabled>1</enabled>
      <blendop_params>GZIP_BASE64_BLOB</blendop_params>
      <blendop_version>14</blendop_version>
      <multi_priority>0</multi_priority>
      <multi_name>STRING</multi_name>
      <multi_name_hand_edited>0</multi_name_hand_edited>
    </plugin>
    <!-- additional plugins... -->
  </style>
</darktable_style>

User-authored entries have <multi_name> empty (""). darktable's auto-applied entries have <multi_name> starting with _builtin_ (e.g., _builtin_scene-referred default, _builtin_auto).

Note (Phase 0 finding): darktable 5.4.1 GUI exports do not include <iop_order> inside <plugin>. Pipeline ordering is reconstructed by darktable from the parent XMP's darktable:iop_order_version and a global iop_list, not from per-plugin metadata. Earlier drafts of this schema listed <iop_order>FLOAT</iop_order> as a sibling of <multi_name_hand_edited>; that was aspirational and has been removed.

contracts/vocabulary-manifest

Per-pack manifest.json. Per 03/Vocabulary primitives:

{
  "name": "tone_lifted_shadows_subject",
  "layer": "L3",
  "subtype": "look",
  "path": "layers/L3/local/tone_lifted_shadows_subject.dtstyle",
  "touches": ["toneequalizer"],
  "tags": ["tone", "shadows", "local", "subject"],
  "description": "Lift shadow zones, restricted to the subject mask.",
  "mask_kind": "raster",
  "mask_ref": "current_subject_mask",
  "global_variant": "tone_lifted_shadows",
  "modversions": {"toneequalizer": 4},
  "darktable_version": "5.4",
  "source": "starter",
  "license": "MIT"
}

Maskdef entries (RFC-032). A fourth vocabulary kind, distinguished by top-level kind: "mask" discriminator. Maskdef entries skip path / touches / modversions (no dtstyle) and instead carry spec (the apply-time mask_spec shape: drawn dt_form / parametric range_filter / both) and an optional llm_vision_prompt declaring the canonical prompt for LLM-vision construction (per llm-vision-for-masks.md Pattern 7). Loaded into a separate index; resolved at apply time via {"kind": "named", "name": "..."} references in the mask_spec field of any primitive entry, MCP apply_primitive / apply_per_region call, or CLI --mask-spec override.

{
  "name": "mask_sky",
  "kind": "mask",
  "description": "Sky region (Heaton-style 'adaptive sky'); top-luminance + blue-hue parametric fallback; LLM-vision route per Pattern 7.",
  "tags": ["mask", "named", "sky", "content-aware", "landscape"],
  "darktable_version": "5.4",
  "source": "expressive-baseline",
  "license": "MIT",
  "spec": {
    "range_filter": {"kind": "luminance", "min": 0.55, "max": 1.0, "feather": 0.08}
  },
  "llm_vision_prompt": "Select the sky region — including clouds, atmosphere, ..."
}

contracts/xmp-darktable-history

darktable's history is an RDF Seq of <rdf:li> elements. Calibrated to darktable 5.4.1; per 04/3 and verified against the v3 Phase 0 reference XMP:

<rdf:li
  darktable:num="N"
  darktable:operation="OPERATION_NAME"
  darktable:enabled="1"
  darktable:modversion="MODVERSION"
  darktable:params="HEX"
  darktable:multi_name="STRING"
  darktable:multi_name_hand_edited="0"
  darktable:multi_priority="0"
  darktable:blendop_version="14"
  darktable:blendop_params="GZIP_BASE64_BLOB"/>

Per-entry execution ordering comes from the parent <rdf:Description>'s darktable:iop_order_version="N" attribute and an internal iop_list, not from per-<rdf:li> metadata. <darktable:history_end> at the parent controls how many entries are applied.

Note (Phase 0 finding): darktable 5.4.1 does not write a darktable:iop_order attribute on <rdf:li> history entries. Earlier drafts of this schema listed it as required-for-new-instances and inherited-for-replacements; that was aspirational. SET-replace under Path A inherits position implicitly because the entry stays at its baseline index. Path B (new-instance addition at a previously-unused multi_priority) currently has no source for iop_order from either dtstyle or XMP — implementations should defer Path B to a follow-up.

contracts/per-image-repo

Per-image directory layout. Per 02/4:

~/Pictures/Chemigram/<image_id>/
  raw/                    symlink to original
  brief.md
  notes.md
  metadata.json           EXIF cache, layer bindings
  current.xmp             synthesized from current snapshot
  objects/                content-addressed snapshot store
    NN/HHHHH...xmp        SHA-256 sharded
  refs/
    heads/<branch>        text file containing snapshot hash
    tags/<tag>            text file containing snapshot hash
    HEAD                  text file: "ref: refs/heads/main" or hash
  log.jsonl               append-only operation log
  sessions/               session transcripts (JSONL per session)
  previews/               render cache (regenerable)
  exports/                final outputs
  vocabulary_gaps.jsonl   gaps surfaced this image

contracts/mcp-tools

The agent-visible MCP tool surface. Grouped by subsystem.

Vocabulary and edit operations - list_vocabulary(layer?, tags?) → entries - list_masks_vocabulary(tags?) → entries (RFC-032 named maskdefs) - get_state(image_id) → entries + head hash - apply_primitive(image_id, primitive_name, mask_spec?, value?, strength?) → state_after, snapshot_hash. value accepts scalar (single-parameter shorthand) or dict (multi-parameter {name: value, ...}) per RFC-021 / ADR-079. strength ∈ [0.0, 1.0] interpolates parameterized L2 looks per RFC-035 / ADR-088. - apply_per_region(image_id, regions, primitive_name?, label?) → state_after, snapshot_hash, n_regions. Single-op shape (RFC-031): top-level primitive_name + each region carries mask_spec + optional parameter_values. Mixed-op shape (RFC-036 / ADR-089): omit top-level primitive_name; each region carries ops: [{primitive_name, parameter_values?}, ...]. Discriminator is presence of ops on any region. Atomic validate-then-apply. - apply_spot(image_id, kind, x, y, radius, source_x?, source_y?, opacity?, border?) → state_after, snapshot_hash (RFC-025 / ADR-087 — heal/clone) - wb_from_gray_card(image_path, x, y, sample_radius) → temperature_coefficients (v1.10.0) - propagate_state(source_image_id, target_image_ids, exclude_ops?, include_per_image?, label?) → results, n_succeeded, n_failed (RFC-037 / ADR-090 — LR-Sync analog). Core Python API takes source_workspace/target_workspaces directly; MCP wrapper resolves image_ids to workspaces. - remove_module(image_id, module_name) → state_after, snapshot_hash - reset(image_id) → state_after (resets to baseline_end, not empty)

Rendering - render_preview(image_id, size=1024, ref_or_hash?) → jpeg_path - compare(image_id, hash_a, hash_b, size=1024) → jpeg_path - export_final(image_id, ref_or_hash?, size=None, format="jpeg") → output_path

Versioning - snapshot(image_id, label?) → hash - checkout(image_id, ref_or_hash) → state - branch(image_id, name, from?) → ref - log(image_id, limit=20) → entries - diff(image_id, hash_a, hash_b) → primitive diffs - tag(image_id, name, hash?) → ref

Masking — the v1.0.0 mask-tool surface (generate_mask, list_masks, regenerate_mask, invalidate_mask, tag_mask) was retired in v1.5.0 per ADR-076. Mask construction now happens inline via mask_spec on apply_primitive / apply_per_region (drawn forms RFC-029 / ADR-084 + parametric range filters RFC-024 / ADR-085 + named maskdefs RFC-032). LLM-vision mask construction lands via the same wire (RFC-026 / ADR-086 — chat-client constructs mask_spec from spatial reasoning).

Ingestion and binding - ingest(raw_path, image_id?, workspace_root?) → image_id, exif_summary, suggested_bindings. workspace_root defaults to the server's configured root (CLI passes the global --workspace flag here). - bind_layers(image_id, l1_template?, l2_template?) → state_after

Context - read_context(image_id) → taste_md + brief_md + notes_md + recent_log - propose_taste_update(content, category, file?) → proposal_id. file defaults to the active taste file selected by the current scope. - confirm_taste_update(proposal_id) → ok - propose_notes_update(image_id, content) → proposal_id - confirm_notes_update(proposal_id) → ok - log_vocabulary_gap(image_id, description, workaround?, intent?, intent_category?, missing_capability?, operations_involved?, vocabulary_used?, satisfaction?, notes?) → ok. Required: image_id, description. Optional fields capture richer gap structure for Phase 2 analytics (per docs/guides/gap-log.md).


constraints

The non-negotiables. Things that hold across all implementation choices.

constraints/single-process

Engine is a single Python process. No daemon, no IPC between subsystems. Each render spawns a darktable-cli subprocess. State is the filesystem.

Anchored from: ADR-006

constraints/serial-renders

Only one darktable-cli instance per configdir at a time. darktable holds an exclusive lock on library.db. The render pipeline must serialize subprocess calls.

Anchored from: ADR-005

constraints/byoa

No AI capabilities bundled with the engine. No PyTorch dependency in chemigram.core. No model weights. AI is provided via MCP-configured providers.

Anchored from: ADR-007

constraints/agent-only-writes

Edit state mutations happen only through the engine's API, called by the agent. The photographer reads previews. The engine never silently mutates state outside an agent-initiated tool call.

Anchored from: ADR-024

constraints/dt-orchestration-only

Chemigram does not implement image-processing capabilities. All color science, lens correction, denoise, tone, mask logic comes from darktable. Chemigram's responsibility is orchestration: vocabulary, composition, versioning, sessions.

Anchored from: ADR-025

constraints/opaque-hex-blobs

op_params and blendop_params are treated as opaque hex/base64 blobs in v1. The synthesizer copies them verbatim from .dtstyle to XMP. Programmatic generation (decoding/encoding the C structs) is reserved for Path C — limited to a small set of high-value modules, only when a clear bottleneck appears.

Anchored from: ADR-008, RFC-016

constraints/modversion-pinning

Vocabulary .dtstyle files are calibrated to a specific darktable version. The manifest's darktable_version field declares this. When darktable updates a module's modversion, captured .dtstyle files become invalid for that module.

Anchored from: ADR-026, RFC-007

constraints/agent-only-mcp

The agent never accesses the engine directly. All operations go through the MCP server. The engine has no agent-aware code.

Anchored from: ADR-006

constraints/local-only-data

Session transcripts, taste evolution, masks, vocabulary gaps — all stay on the photographer's machine. No telemetry. No phone-home. No cloud dependency.

Anchored from: ADR-027


stack

Locked technology choices. Each entry has a corresponding ADR.

Layer Choice ADR
Image processor darktable 5.x (CLI for rendering) ADR-014
Language Python 3.11+ ADR-013
Agent protocol MCP (Model Context Protocol) ADR-006
Vocabulary format darktable .dtstyle XML ADR-008
Edit state darktable XMP RDF/XML ADR-008
Versioning storage Filesystem, content-addressed by SHA-256 ADR-018
Configuration TOML (config.toml) ADR-028
Manifest format JSON (manifest.json) ADR-028
Mask format Drawn forms encoded into XMP masks_history (gradient/ellipse/rectangle) ADR-076
Prompt template engine Jinja2 ADR-043
Active prompt version registry TOML (MANIFEST.toml) ADR-044
Eval run manifests JSON + JSONL history ADR-047
Golden eval datasets Versioned directories (golden_v1, golden_v2, ...) ADR-046
Session transcripts JSONL ADR-029
Default mask path Drawn-form geometry only (no PNG, no provider) ADR-076
Lens correction Lensfun (via darktable) + embedded EXIF metadata ADR-014
Noise model darktable profiled denoise ADR-014
Color science darktable scene-referred pipeline ADR-014
Subject masking (coarse, MVP) LLM-vision-as-provider via the chat-client (RFC-026 / ADR-086) — Claude.ai / ChatGPT / Claude Code see render_preview JPEGs and construct mask_spec RFC-026
Subject masking (precision tier, deferred) chemigram-masker-sam sibling project — SAM-class, deployed RFC-030
Build backend hatchling (via pyproject.toml) ADR-034
Package layout src/-style, single distribution, two modules (chemigram.core, chemigram.mcp) ADR-034
Dev environment uv (lockfile: uv.lock) ADR-035
Test framework pytest, three tiers (unit / integration / e2e) ADR-036
Linter and formatter ruff (ruff check + ruff format) ADR-037
Type checker mypy (strict on chemigram.core) ADR-038
Pre-commit hooks pre-commit framework (ruff + mypy + unit tests on push) ADR-039
CI GitHub Actions, Python 3.11/3.12/3.13, macOS-only for v1 ADR-040
Versioning SemVer (0.x during Phase 1, 1.0.0 at Phase 1 done) ADR-041
Distribution PyPI primary; GitHub releases supplement; TestPyPI for early validation ADR-042

map — RFC and ADR state board

The canonical state board for the tech plane. When an RFC closes into an ADR, both tables update; the corresponding stack/component sections update too if affected.

RFCs

RFC Title Status Closes into
RFC-001 XMP synthesizer architecture Decided ADR-050 (closes Path A); ADR-063 closes the Path B / iop_order question
RFC-002 Canonical XMP serialization for stable hashing Decided ADR-054 (closes)
RFC-003 Mask storage in versioning Decided ADR-055 (closes); superseded by ADR-076 (path retired)
RFC-004 Default masking provider — coarse vs SAM Decided ADR-058 (closes); superseded by ADR-076 (path retired)
RFC-005 Pipeline stage protocol — abstract now or YAGNI Decided ADR-052 (closes)
RFC-006 Same-module collision behavior Decided ADR-051 (closes)
RFC-007 modversion drift handling Decided ADR-082 (closes)
RFC-008 Vocabulary discovery at scale Draft v0.1 (speculative)
RFC-009 Mask provider protocol shape Decided ADR-057 (closes); superseded by ADR-076 (path retired)
RFC-010 MCP tool surface — parameter shapes and error contracts Decided ADR-056 (closes)
RFC-011 Agent context loading order and format Decided ADR-059 (closes)
RFC-012 Programmatic vocabulary generation (Path C) Decided ADR-073 (closes)
RFC-013 Vocabulary gap surfacing format Decided ADR-060 (closes)
RFC-014 End-of-session synthesis flow Decided ADR-061 (closes)
RFC-015 EXIF auto-binding rules Decided ADR-053 (closes)
RFC-016 Versioned prompt system Decided ADR-043, ADR-044, ADR-045
RFC-018 Vocabulary expansion for expressive taste articulation Decided ADR-063, ADR-064, ADR-073
RFC-019 Reference-image validation baseline Decided ADR-066, ADR-067, ADR-068
RFC-020 Command-line interface for Chemigram Decided ADR-069, ADR-070, ADR-071, ADR-072 (closes)
RFC-021 Parameterized vocabulary magnitudes (Path C default for continuous-magnitude modules) Decided ADR-077, ADR-078, ADR-079, ADR-080 (closes)
RFC-022 Bulk parameterization of common-use darktable modules (tiered baseline) Decided ADR-081 (closes; explicitly amends ADR-008)
RFC-023 HSL Color Mixer parity (colorzones vs colorequal backing) Decided ADR-083 (closes)
RFC-024 Range masks (color-range / luminance-range / depth-range / subject) Decided ADR-085 (closes; depth + subject deferred to RFC-026)
RFC-025 Spot removal / heal architecture (retouch byte serialization) Decided ADR-087 (closes; AI auto-detection deferred to RFC-030)
RFC-026 LLM-vision-as-provider for AI-derived masks (MVP, conversation-native) Decided ADR-086 (closes)
RFC-029 Compositional masks at apply time (build-by-words) Decided ADR-084 (closes)
RFC-030 Deployed sibling-provider scaffolding for precision-tier AI masks Draft v0.1 (deferred)
RFC-031 Batched per-region adjustment meta-tool Draft v0.1 (impl shipped) — (pending)
RFC-032 Named-mask vocabulary on v1.9.0 mask primitives Draft v0.1 (impl shipped) — (pending; possibly two ADRs)
RFC-033 Skin-tone uniformity primitive Draft v0.1 (Path B impl shipped; gated on visual review) — (pending; path depends on visual review)
RFC-034 invert flag on named-mask references Draft v0.1 (parametric impl shipped) — (pending)
RFC-035 Parametric L2 strength (opacity-as-amount) Decided (Path B); ADR-088 Draft until darkroom validation ADR-088 (closes)
RFC-036 Mixed-op apply_per_region (un-defer of RFC-031) Decided; ADR-089 Draft until darkroom validation ADR-089 (closes)
RFC-037 propagate_state MCP verb (anchor-and-sync workflow) Decided; ADR-090 Draft until darkroom validation ADR-090 (closes)
RFC-038 Mode B autonomous session protocol Draft v0.1 (v1.11+ pick) — (pending)

ADRs

ADR Title Status
ADR-001 Vocabulary approach (Architecture B) Accepted
ADR-002 SET semantics: replace by (operation, multi_priority) Accepted
ADR-003 Three foundational disciplines (writer, dt-photography, BYOA) Accepted
ADR-004 darktable-cli invocation form Accepted
ADR-005 Subprocess serialization per configdir Accepted
ADR-006 Single Python process, MCP server, no daemon Accepted
ADR-007 BYOA — no bundled AI capabilities Accepted
ADR-008 XMP and .dtstyle as opaque-blob carriers Accepted
ADR-009 Path A vs Path B for synthesis Accepted
ADR-010 Vocabulary parser identifies user entries by empty <multi_name> Accepted
ADR-011 Reject darktable-cli --style NAME for vocabulary application Accepted
ADR-012 --apply-custom-presets false always Accepted
ADR-013 Python 3.11+ Accepted
ADR-014 All image-processing via darktable Accepted
ADR-015 Three-layer model (L0/L1/L2/L3) Accepted
ADR-016 L1 empty by default; opt-in per camera+lens Accepted
ADR-017 L2 has two flavors (neutralizing, look-committed) Accepted
ADR-018 Per-image content-addressed DAG Accepted
ADR-019 Git-like ref structure (objects/, refs/heads, refs/tags, HEAD) Accepted
ADR-020 No remote, no three-way merge, no reflog Accepted
ADR-021 Three-layer mask pattern (pre-baked, AI-raster, agent-described) Superseded by ADR-076
ADR-022 Mask registry per image with symbolic refs Superseded by ADR-076
ADR-023 Vocabulary primitives are .dtstyle + manifest entries Accepted
ADR-024 Authoring discipline: uncheck non-target modules in dialog Accepted
ADR-025 WB and color calibration coupling — author with both or decouple Accepted
ADR-026 Vocabulary modversion-pinned to darktable version Accepted
ADR-027 Local-only session data — no telemetry, no cloud Accepted
ADR-028 Configuration formats: TOML for config, JSON for manifests Accepted
ADR-029 Session transcripts as JSONL with header metadata Accepted
ADR-030 Three-tier context model (taste/brief/notes) Accepted
ADR-031 Propose-and-confirm for context updates Accepted
ADR-032 Distribution split (OSS engine, OSS starter, OSS packs, private personal) Accepted
ADR-033 MCP tool surface (initial) Accepted
ADR-034 Build system and package layout Accepted
ADR-035 Dev environment with uv Accepted
ADR-036 Testing strategy: pytest with three tiers Accepted
ADR-037 Linting and formatting with ruff Accepted
ADR-038 Type checking with mypy strict for core Accepted
ADR-039 Pre-commit hooks for local quality gates Accepted
ADR-040 CI on GitHub Actions, macOS-only for v1 Accepted
ADR-041 SemVer with 0.x for Phase 1 development Accepted
ADR-042 Distribution via PyPI, GitHub releases as supplement Accepted
ADR-043 Jinja2 + filename-versioned templates as prompt format Accepted
ADR-044 PromptStore API and MANIFEST.toml as active-version registry Accepted
ADR-045 Prompt versioning is independent of package SemVer Accepted
ADR-046 Golden dataset versioning (immutable, append-only) Accepted
ADR-047 Run manifests for eval reproducibility Accepted
ADR-048 Multi-scope taste structure (extends ADR-030) Accepted
ADR-049 Vocabulary-starter ships within chemigram (clarifies ADR-032) Accepted
ADR-050 Parser API and synthesizer error contract (closes RFC-001) Accepted
ADR-051 Same-module SET-replace; last-writer-wins; Path B deferred (closes RFC-006) Accepted
ADR-052 PipelineStage Protocol with single v1 stage / DarktableCliStage (closes RFC-005) Accepted
ADR-053 EXIF auto-binding by exact-match identity (closes RFC-015) Accepted
ADR-054 Canonical XMP serialization for stable content hashing (closes RFC-002) Accepted
ADR-055 Raster masks share objects/ store; masks/registry.json maps names (closes RFC-003) Superseded by ADR-076
ADR-056 MCP tool surface: parameter shapes + error contract (closes RFC-010) Accepted
ADR-057 MaskingProvider Protocol shape (closes RFC-009) Superseded by ADR-076
ADR-058 Default masking provider: CoarseAgentProvider (closes RFC-004) Superseded by ADR-076
ADR-059 Agent context loading order and format (closes RFC-011) Accepted
ADR-060 Vocabulary gap JSONL schema (closes RFC-013) Accepted
ADR-061 End-of-session synthesis is agent-orchestrated (closes RFC-014) Accepted
ADR-062 Reset rewinds the current branch to baseline Accepted
ADR-063 Path B unblocking; iop_order resolved as None (closes RFC-001, RFC-018) Accepted
ADR-064 Vocabulary authoring workflow (post-v0.2) (closes RFC-018) Accepted
ADR-066 Reference fixture policy (synthetic-only) (closes RFC-019) Accepted
ADR-067 Pixel-level assertion protocol (closes RFC-019) Accepted
ADR-068 darktable version gate (deferred) (closes RFC-019) Accepted (deferred)
ADR-069 CLI alongside MCP, won't replace it (closes RFC-020) Accepted
ADR-070 CLI framework: Typer (closes RFC-020) Accepted
ADR-071 CLI–MCP–core thin-wrapper discipline (closes RFC-020) Accepted
ADR-072 CLI output format: human default, NDJSON via --json (closes RFC-020) Accepted
ADR-073 Programmatic vocabulary authoring via reverse-engineered structs (closes RFC-012) Accepted
ADR-074 Built-in geometric mask providers (gradient/radial/rectangle) Superseded by ADR-076
ADR-075 CI matrix expands to Ubuntu alongside macOS (amends ADR-040) Accepted
ADR-076 Drawn-mask-only mask architecture (supersedes 021/022/055/057/058/074) Accepted
ADR-077 Path C as default for parameterized modules (closes RFC-021; partially supersedes ADR-008) Accepted
ADR-078 Vocabulary manifest parameters schema, multi-parameter from day one (closes RFC-021) Accepted
ADR-079 apply_primitive value / param argument shape; hard-reject range validation (closes RFC-021) Accepted
ADR-080 Test-coverage policy for parameterized modules; hard CI gate (closes RFC-021) Accepted
ADR-081 Parameterization tiering policy; closes RFC-022, explicitly amends ADR-008 Accepted
ADR-082 Modversion drift handling: warn-loud at load, hard-fail at apply (closes RFC-007) Accepted
ADR-083 HSL Color Mixer via colorequal (3 multi-axis entries); closes RFC-023, reclassifies HSL Tier 2 Accepted
ADR-084 Apply-time mask spec semantics + path shape addition; closes RFC-029, formalizes build-by-words inline mask construction Accepted
ADR-085 Parametric mask encoding via blendif; range_filter mask_spec field; AND composition with drawn masks (closes RFC-024) Accepted
ADR-086 LLM-vision-as-provider for AI-derived masks (MVP via existing wire + workflow guide; closes RFC-026; precision tier deferred to RFC-030) Accepted
ADR-087 Retouch byte encoding + apply_spot MCP tool (closes RFC-025; HEAL+CLONE on CIRCLE geometry, single-form per call; AI auto-detection deferred to RFC-030) Accepted
ADR-088 Parametric L2 strength via Path B (per-parameter interpolation); closes RFC-035 Draft (impl shipped; flips to Accepted on darkroom validation)
ADR-089 Mixed-op apply_per_region schema extension; closes RFC-036 Draft (impl shipped; flips to Accepted on darkroom validation)
ADR-090 propagate_state MCP verb (anchor-and-sync workflow); closes RFC-037 Draft (impl shipped; flips to Accepted on darkroom validation)

Changelog

  • v0.1 · 2026-04-27 · Initial population from 04 + Phase 0 findings + concept package work. RFCs and ADRs draft-numbered to match TA/map.

TA · v0.1 · This is a reference document. Read by linking-into specific sections.