Skip to content

feat(templates): GUI template authoring + per-instance template folders - #24

Merged
Xander-git merged 2 commits into
mainfrom
feat/template-authoring-gui
May 31, 2026
Merged

feat(templates): GUI template authoring + per-instance template folders#24
Xander-git merged 2 commits into
mainfrom
feat/template-authoring-gui

Conversation

@Xander-git

Copy link
Copy Markdown
Collaborator

Summary

Lets operators author Copier templates entirely in the GUI, and scopes templates per instance so each created project/run carries its own template folder. Implements the approved design at docs/superpowers/specs/2026-05-30-template-authoring-gui-design.md.

The hierarchy stays equipment → project → run (no new "experiment" level). Single feature commit on top of main (which already includes the distribution/#23 work).

What's included

GUI template authoring (new template/ service layer, pure / no NiceGUI):

  • manifest.py — typed TemplateManifest with round-tripping from_yaml/to_yaml (now hosts TemplateQuestion/template_questions; templates.py re-exports them).
  • lint.py — single source of validation (LintFinding error/warn) factored out of TemplateEngine.resolve, plus a Jinja2 .jinja parse check. resolve re-raises the same TemplateLoadError/TemplateCoreFieldRedeclaredError on the same conditions (regression-tested).
  • authoring.py — atomic writes via atomic_write_bytes; one _safe_target path-traversal chokepoint (reuses paths.project_name_violations); upload size/count caps; opt-in .jinja; stale-edit guard.
  • ui/pages/template_editor.py + /templates/edit route; manager gains a per-row Edit action and a scope/location selector.

Per-instance template folders (.exlab-wizard/templates/{equipment|project|run}/):

  • resolution.py — nearest-wins chain (project → equipment → global), run-scope narrowing, TemplateChoices bundle, reconcile_selection.
  • provenance.py — after CACHE_WRITE, copies the rendering template into the instance's own typed subfolder; creation.json gains template.provenance_path (CREATION_JSON_VERSION 1.9 → 1.10). POST_VALIDATE skips .exlab-wizard/ so the frozen copy isn't validated as run output.

Reactive wizard wiring:

  • Project/run wizards re-resolve their template list from the operator's equipment/project selection and submit the resolved absolute path (not templates_dir/name), so a per-instance template renders the right source. A review-caught bug — a same-named per-instance override leaving the stored path stale — is fixed via the shared reconcile_selection helper.

Robustness

Validate-on-save/lint · atomic & safe writes · path-traversal + upload caps · don't-break-templates-in-use (frozen provenance copy + version-bump guard). Copier stays unsafe=False/skip_tasks=True.

Testing

New units: lint / authoring / manifest / resolution / provenance; editor + mount UI; a creation-provenance integration test. Verified per-suite (sync excluded — known flakiness): 425 tests pass (template 143, mount 185, wizards 13+13, editor 8, templates-page 20, controller-integration 43). uvx ruff check + format --check clean.

uv run --extra test pytest tests/unit/template tests/unit/ui/test_mount.py \
  tests/unit/ui/test_template_editor.py tests/integration/controller

Out of scope (spec §7 — future work)

Raw-YAML advanced editor tab · equipment-template rendering through Copier · opt-in provenance-copy config flag.

🤖 Generated with Claude Code

Xander-git and others added 2 commits May 30, 2026 13:10
Lets operators author Copier templates entirely in the GUI and scopes
templates per instance. Approved design:
docs/superpowers/specs/2026-05-30-template-authoring-gui-design.md.

Authoring (new template/ service layer, no NiceGUI):
- manifest.py: typed TemplateManifest with round-tripping from_yaml/to_yaml
  (hosts TemplateQuestion/template_questions; templates.py re-exports them).
- lint.py: single-source validation (LintFinding error/warn) factored out of
  TemplateEngine.resolve + a Jinja2 .jinja parse check; resolve re-raises the
  same TemplateLoadError/TemplateCoreFieldRedeclaredError on the same codes.
- authoring.py: atomic writes via atomic_write_bytes, one _safe_target
  path-traversal chokepoint (reuses paths.project_name_violations), upload
  size/count caps, opt-in .jinja, stale-edit guard.
- ui/pages/template_editor.py + /templates/edit route + manager Edit action
  and scope/location selector.

Per-instance template folders (.exlab-wizard/templates/{equipment|project|run}/):
- resolution.py: nearest-wins chain (project -> equipment -> global),
  run-scope narrowing, TemplateChoices bundle, reconcile_selection.
- provenance.py: post-CACHE_WRITE frozen copy of the rendering template into
  the instance's own typed subfolder; creation.json gains template
  .provenance_path (CREATION_JSON_VERSION 1.9 -> 1.10); POST_VALIDATE skips
  .exlab-wizard/ so the copy isn't validated as run output.

Reactive wizard wiring (Phase 5):
- Project/run wizards re-resolve their template list from the operator's
  equipment/project selection and submit the resolved absolute path (not
  templates_dir/name), so a per-instance template renders the right source.

Tests: lint/authoring/manifest/resolution/provenance units, editor + mount
units, and a creation-provenance integration test. Full feature suite green;
ruff clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
CI feedback on PR #24:

- lint job (ruff format --check): reformat template/lint.py.
- lint job (mypy): the Phase-5 reactive handlers used a
  `lambda e: (setattr(...), cb())` tuple idiom; mypy rejects setattr in an
  expression (func-returns-value). Replaced with named nested handlers in
  both wizards (also clearer per review feedback).
- coverage job (91% floor; was 90.02%): template_editor.py was only 17%
  covered (headless payload path only). Added a fake-NiceGUI `ui` surface
  that records widgets + click/upload handlers by data-testid, exercising
  the rich render branch end to end: lint banners (clean/warn/error),
  file list + inline edit/save/delete, codemirror vs textarea fallback,
  question add/remove/save with default coercion (int/float/bool/choice +
  uncoercible fallback), upload (with/without callback, unavailable build),
  unreadable-manifest and unreadable-file branches, and Back. Editor
  coverage 17% -> 99%.

ruff check/format + mypy clean; affected suites green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@Xander-git
Xander-git merged commit 10b8829 into main May 31, 2026
7 of 8 checks passed
@Xander-git
Xander-git deleted the feat/template-authoring-gui branch May 31, 2026 01:12
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant