ADR 0018: LayoutSpec and ComplianceReport live in core; imaging does not persist¶
Status¶
Accepted — 2026-09-25
Supersedes the layout-spec bullet of ADR 0006, which placed the Pydantic model in templates/schema.py. Every other decision in ADR 0006 — Jinja2 with StrictUndefined, TOML layout files, immutable versioned template rows, the builtin set, the commands — stands unchanged.
Context¶
The dependency rule (AGENTS.md, enforced by import-linter) says core imports no other internal package and every adapter package may import core only. The Phase 4 and Phase 5 specs as first written broke it twice:
- ADR 0006 put
LayoutSpecintemplates/schema.py, butimaging/overlay.py(Phase 5) andHeroServiceincore.services(Phase 6) both consume it. Neither may importtemplates. - The Phase 5 spec put
finalize()incore/services/compliance.py, callingimaging,LayoutSpec,AssetStore(storage) andOutputSettings(settings). Thecore is purecontract forbids all four.
The same problem was solved before: MetadataSource moved to core/sources.py (P2.3) and the ImageProvider boundary models to core/providers.py (P3.1) because core.services consumes them.
Decision¶
LayoutSpecand its blocks live insrc/thumbforge/core/layout.py.templates/schema.pyonly loads TOML into it (load_layout(path) -> LayoutSpec).ComplianceReportlives insrc/thumbforge/core/models.py.imaging/finalize.py::render_final(raw, layout, output, *, title, part_number, part_label) -> tuple[bytes, ComplianceReport]does pixels only: fit, overlay, encode, then check. It touches neither the database nor the asset store.- P6.1's
HeroServicereceivesrender_finalas an injected callable and stores the result: thefinalasset,compliant,compliance_report_json, andComplianceErrorhandling. Its collaborators arrive as Protocols, asFetchService's do. imagingandtemplatesjoin theadapters are independentimport-linter contract.
Consequences¶
- import-linter keeps passing with no exceptions carved out.
- Persistence is in one place (the service), so hero and batch runs record compliance the same way.
render_finalcan be tested with no database: bytes in, bytes and a report out.- Specs updated:
docs/specs/phase-4-templates.md,phase-5-imaging.md,phase-6-hero.md,phase-7-batch.md; ROADMAP P4.1, P5.3 (now depends on P5.2) and P6.1.
Alternatives considered¶
- Keep
finalizeincoreand inject fit, overlay and check through anImagingProtocol — rejected: one Protocol with four methods and one implementation, only to keep a function thatimagingcan own outright. - Relax
core is pureforimaging— rejected: it reopens the dependency rule for one caller and makescoreuntestable without Pillow. - Keep
LayoutSpecintemplatesand letimagingimporttemplates— rejected: adapters would depend on each other, andcore.servicesstill could not see the type.