Skip to content

feat(presets): Composition strategies (prepend, append, wrap) for templates, commands, and scripts #2132

@mnriem

Description

@mnriem

Summary

Add composition strategies to the preset system so that presets can augment core templates instead of only replacing them. Today, PresetResolver returns the first match in the priority stack — a pure "replace" strategy. This proposal adds prepend, append, and wrap strategies that let presets inject content before, after, or around the lower-priority template.

Listed as a future consideration in the presets README and explicitly deferred as out of scope in #1708.

Problem Statement

The current preset system only supports full replacement. If two presets both provide spec-template, the highest-priority one wins entirely. This forces preset authors to duplicate the full content of the template they want to extend, even for a single added section.

Scenario Current behavior Desired behavior
Healthcare preset adds a "PHI Considerations" section to spec template Must copy entire core spec-template.md and add the section strategy: "append" with only the new section
Enterprise preset wraps the plan command with compliance preamble/sign-off Must copy entire plan.md command and add wrapper strategy: "wrap" with {CORE_TEMPLATE} placeholder
CI preset runs validation before create-new-feature.sh Not possible — scripts can only be replaced strategy: "wrap" with $CORE_SCRIPT variable

This is especially problematic when core templates evolve (replaced presets don't get upstream improvements) and when multiple additive presets need to stack.

Proposed strategy field in preset.yml

provides:
  templates:
    - type: "template"
      name: "spec-template"
      file: "templates/spec-addendum.md"
      strategy: "append"        # new optional field, default: "replace"

    - type: "command"
      name: "plan"
      file: "commands/plan-wrapper.md"
      strategy: "wrap"

    - type: "script"
      name: "create-new-feature"
      file: "scripts/create-new-feature-wrapper.sh"
      strategy: "wrap"

Supported combinations:

Type replace prepend append wrap
template ✓ (default)
command ✓ (default)
script ✓ (default)

Scripts only support replace and wrap (prepend/append don't make semantic sense for executable code).

Strategy semantics

  • replace (default): No change — preset's template fully replaces lower-priority one.
  • prepend: Preset content placed before resolved lower-priority template, separated by a blank line.
  • append: Preset content placed after resolved lower-priority template, separated by a blank line.
  • wrap: For templates/commands — preset content contains {CORE_TEMPLATE} placeholder replaced with resolved lower-priority content. For scripts — $CORE_SCRIPT variable points to the lower-priority script path.

Resolution changes

Current PresetResolver.resolve() returns Optional[Path] — just a file path. With composition, the result is synthesized content from multiple files. New API:

class PresetResolver:
    def resolve(self, template_name, template_type="template") -> Optional[Path]: ...           # existing
    def resolve_content(self, template_name, template_type="template") -> Optional[str]: ...    # new

Composition is recursive — multiple composing presets chain:

Priority 1: preset-a (strategy: "prepend", security header)
Priority 2: preset-b (strategy: "append", compliance footer)
Priority 4: core spec-template

→ Result: <security header> + <core content> + <compliance footer>

Impact areas

Implementation File Changes needed
Python resolver src/specify_cli/presets.py (PresetResolver, ~line 1710) Add resolve_content(), manifest-aware strategy lookup
Bash resolver scripts/bash/common.sh (resolve_template()) Add resolve_template_content()
PowerShell resolver scripts/powershell/common.ps1 (Resolve-Template) Add Resolve-TemplateContent
Manifest validation src/specify_cli/presets.py (PresetManifest._validate(), ~line 96) Validate strategy values; reject prepend/append for scripts
Command registration src/specify_cli/presets.py (_register_commands(), ~line 691) Compose before writing to agent directories
Scaffold presets/scaffold/preset.yml Document strategy field

Acceptance Criteria

  • strategy field accepted in preset.yml with values: replace (default), prepend, append, wrap
  • PresetManifest._validate() validates strategy; rejects prepend/append for type: "script"
  • PresetResolver.resolve_content() returns composed content
  • resolve_template_content() added to bash and PowerShell scripts
  • wrap replaces {CORE_TEMPLATE} for templates/commands, sets $CORE_SCRIPT for scripts
  • Multiple composing presets chain correctly (recursive resolution)
  • replace remains default; existing presets work without changes
  • Command registration handles composition at install time
  • specify preset resolve <name> shows composition chain
  • Unit tests for each strategy × type combination and multi-preset chains
  • Documentation updated: presets/README.md, presets/ARCHITECTURE.md, scaffold

Out of Scope

  • Structural merge strategies (parsing Markdown sections)
  • Per-section granularity (e.g., "replace only ## Security")
  • Conflict detection UI (specify preset lint / specify preset doctor)

References

Metadata

Metadata

Assignees

Type

No type
No fields configured for issues without a type.

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions