Study VII — Lazy pieces¶
Date: 2026-08 · Status: study — feeds ADR-0014
Owner's requirement (roadmap lane F): a template can offer pieces — fully decoupled units of code you may not want at creation time but can bring in later, with update capability and their own sub-customization; choosable at creation or added after the project exists; distinguishing pieces of one template from pieces common to several.
Concept¶
| Feature | Piece | |
|---|---|---|
| When | render time only | at creation or any time later |
| Mechanism | toggles content baked into the base tree | adds a decoupled unit to a living project |
| Lifecycle | none (part of the render) | own version anchor, updates independently |
| Sub-customization | none (shares template variables) | own variables, recorded per piece |
Anti-explosion rule inherited from ADR-0011: anything expressible as a feature must stay a feature. A piece must justify its independent lifecycle.
The design¶
A piece is a directory + piece.yml¶
Template-local pieces live in the form repo under pieces/<name>/:
android/single-module/
template.yml
app/…
pieces/
room/
piece.yml
app/src/main/kotlin/es/sebas1705/androidsingleapp/data/…
# piece.yml
schema_version: 1
name: room
description: Room persistence layer
variables: # sub-customization, recorded per piece
- key: db_name
default: app
patches: # the ONLY way to touch existing files
- file: gradle/libs.versions.toml
op: add
path: /libraries/room-runtime
value: { module: "androidx.room:room-runtime", version.ref: "room" }
- No
fileslist: everything in the piece directory exceptpiece.ymlis the piece. - No
identitysection: a template-local piece is written in its form's canonical identity; the engine renames it with the identity map of the form manifest plus the project's recorded variable values (from the answers file) plus the piece's own variables. - Directives:
tpl:varagainst piece variables; fulltpl:ifagainst features postponed (a piece is its own unit — conditional content inside it is a smell).
Decoupling is enforced, not hoped for¶
Applying a piece writes its rendered files into the project only if none of those paths already exist — a collision is an error naming the offending path. Wiring into shared files (settings.gradle includes, version catalogs, package.json) happens exclusively through declared patches. Consequences:
- No three-way merge on add — adds are trivial and safe.
- Piece-vs-piece ordering is irrelevant for files (they cannot collide with anything, including each other); patches apply in add order, which the answers file records.
- The unit stays honest: if a piece "needs" to rewrite a project file, it is not a piece — it is a feature or a template change.
The answers file grows a pieces: section (additive)¶
pieces:
- name: room
source: github.com/Templetry/android@main/single-module/pieces/room
commit: <sha resolved at add time>
variables: {db_name: app}
files: [gradle/…, app/src/…] # what the piece owns on disk
commitis the piece's own drift anchor:templetry updatere-renders the piece at head with the recorded inputs and runs the same diff/three-way machinery as template updates (lane A's package) — pieces update independently of the base template.filesrecords ownership, mirroring the ADR-0005 wisdom: removal is not in v1, but recording the file list from day one keeps the door open (a futureremovedeletes owned files; reversing patches remains the researched part).
Discovery needs no registry change (v1)¶
Pieces ship inside the form repo, so templetry pieces lists them by fetching the form tarball — zero registry schema change. The form render excludes pieces/** (like template.yml itself); the form still compiles with the directory present because nothing wires it in.
Common pieces are phase 2¶
A common piece (shared across templates) needs what template-local pieces get for free: its own canonical identity map, a compatibility declaration (which parents/forms or capability tags), and a discovery channel (registry v2.1, additive pieces entry). Per-ecosystem reality (a Room piece only means something on Android/KMP) suggests common pieces will mostly be per-parent, which the compat declaration expresses. Deferred until template-local pieces have proven the lifecycle.
Resolved open questions¶
| Question (roadmap) | Resolution |
|---|---|
| Piece-vs-piece ordering/conflicts | Files can't conflict (existence check); patches apply in recorded add order |
| Sub-customization casings colliding with project identity | Piece variables render through the same Expand; the planner's longest-first replacement order and the existing collision detection apply unchanged |
| Can a piece be removed? | Not in v1; the recorded files: list keeps it possible later |
| Common pieces per ecosystem | Phase 2, compat-declared, mostly per-parent |
| CI cost | One extra job per piece (render base → add piece → compile); linear, not combinatorial, because pieces are decoupled by construction |
Engine surface (all additive)¶
- New package
piece/: load/validatepiece.yml,Apply(projectDir, …), and the answerspieces:read/write. - Planner: exclude
pieces/**from form renders. - CLI:
templetry pieces [dir](list what the project's template offers, marking applied ones) ·templetry add <piece> [dir] [--set k=v]·templetry updategrows piece awareness. - MCP:
list_pieces,add_piecetools; update tool reports pieces too. - Desktop: pieces panel in Local's project preview (list/add/update) — after the engine lands.
Finding from implementation (2026-08-12): the piece-socket boundary¶
Patches only touch structured formats — and Gradle wiring lives in .kts code files. A Room piece for android/single-module therefore cannot wire itself via patches (adding to libs.versions.toml works; implementation(libs.room.runtime) in build.gradle.kts does not). The documented path: forms are authored piece-ready with conditional hooks — if (file("piece.deps.gradle.kts").exists()) apply(from = …) — whose target file the piece owns. Sockets are honest (visible in the form, compile when absent) and keep the no-overwrite rule intact. The android piece waits for its form to grow sockets; the web ecosystem needed none.
Verification plan¶
- Unit: piece load/validate, collision refusal, answers round-trip.
- A real piece on
web/react-spa(npm-buildable locally end to end): render base →add→ build → mutate template →updatepicks the piece. - A real piece on
android/single-module(room) verified by its parent CI job.