Skip to content

Author Extension Architecture

Architectural reference for VIBE's template authoring system. Optimized for LLM consumption.

See also:

  • core.md - Core VIBE engine (probing, handlers, sessions, templates)
  • frontend.md - Frontend UI patterns (htmx, Alpine.js CSP)
  • components.md - Component and definition system
  • design-system.md - Visual design system

1. SYSTEM OVERVIEW

The Author extension provides browser-based template editing. Authors create and modify templates (questions + Jinja2 content) through an htmx-driven two-pane workspace. Both Markdown and DOCX templates are supported.

Core Philosophy:

  • Templates are the source of truth — files on disk (config.yml + template.md/docx)
  • Write path is filesystem-only: file://-backed templates are writable, git:// are read-only
  • Round-trip YAML: comments and formatting survive programmatic edits (ruamel.yaml)
  • Graceful degradation: broken templates can still be opened and fixed in-place

Key Files:

File Purpose
vibe/author/__init__.py Extension entry point
vibe/author/auth.py Authorization (is_author, @author_required)
vibe/author/web/__init__.py Web extension registration (AuthorWebExtension)
vibe/author/web/routes.py All routes (~30 endpoints)
vibe/author/services/template_source.py Read/write template bundles
vibe/author/services/question_schema.py Question validation and normalization
vibe/author/services/transform.py Selection-based transforms
vibe/author/services/docx_projection.py DOCX XML ↔ plain-text projection
vibe/author/services/interview.py Scoped interview sessions for preview
vibe/author/static/author.js Editor toolbar, transforms, drag-drop, DOCX upload
vibe/author/static/author.css Workspace layout and styling

2. EXTENSION REGISTRATION AND AUTH

2.1 Registration

Location: vibe/author/__init__.py, vibe/author/web/__init__.py

Follows standard extension pattern. register_extension() calls register_core_extension("author") and registers AuthorWebExtension, which mounts the blueprint at /author and adds the author/ template folder to Jinja's ChoiceLoader.

Enabled via config:

extensions:
  - vibe.author
dev_settings:
  author: true     # For local development without tokens

2.2 Authorization

Location: vibe/author/auth.py

  • is_author() -> bool — Checks session grants for "author" (from PASETO token vibe_grants) OR dev_settings.author flag
  • @author_required — Route decorator, aborts 403 if not author
  • is_author registered as Jinja global for conditional UI (e.g., showing "Author" links in navigation)

3. WORKSPACE LAYOUT

3.1 Two-Pane Design

Location: vibe/author/templates/author/workspace.html

┌─────────────────────────────────────────────────────────┐
│ Toolbar: Back | Title | Validate                        │
├─────────────────────────┬───────────────────────────────┤
│  Question List          │                               │
│  [Add question]         │   Embedded Interview          │
│  ─── drag handle ───    │   (iframe to /author/         │
│  Question cards         │    <id>/interview)            │
│  (view / edit mode)     │                               │
├ ─ ─ resizable ─ ─ ─ ─ ─┤   Shows live interview with   │
│  Template Source        │   rendered preview + download  │
│  [Toolbar: H1 Bold ...] │                               │
│  <textarea>             │                               │
│  (or DOCX projection)  │                               │
└─────────────────────────┴───────────────────────────────┘

Left column is split vertically (questions top, editor bottom) with a resizable divider. The column divider between left and right is also resizable. Both dividers persist layout via localStorage.

3.2 Embedded Interview Preview

Location: vibe/author/services/interview.py, vibe/author/templates/author/partials/interview_pane.html

The right pane embeds the real VIBE interview in an <iframe> pointing to /author/<template_id>/interview. This reuses the core interview logic (handle_start_interview, handle_process_htmx) with author-scoped session state stored in session["author_interviews"][template_id].

Key integration:

  • author_interview_request() context manager temporarily binds g.interview, g.template_data, g.version_url_adapter for the request, then restores previous values
  • _author_version_url_for() remaps core interview endpoints to author namespace (e.g., interview.process_htmxauthor.author_interview_process)
  • X-Frame-Options: SAMEORIGIN set for author interview routes in vibe/core.py (all other routes use DENY)
  • Interview layout hides site chrome (brand, toolbar) via g.author_embedded = True

4. TEMPLATE SOURCE SERVICE

4.1 Reading Templates

Location: vibe/author/services/template_source.py

Primary loader: load_template_source(provider, template_id, refresh=False) -> TemplateSource

  1. Calls provider.get_template_data() for full validation
  2. On TemplateInvalidError, falls back to _load_source_from_raw() — reads config.yml and template file directly from disk so authors can fix broken templates in-place
  3. Returns TemplateSource dataclass with questions, content, validation status, writability flag

Writability: is_writable(template_data) checks bundle_base_path.startswith("file://"). Git-backed templates display a read-only indicator.

4.2 Writing Templates

Key functions:

  • load_editable_template_files()EditableTemplateFiles with round-trip YAML document (ruamel.yaml typ="rt")
  • write_config_document(files) — Serializes YAML preserving comments
  • write_question_document(files, qid, question, previous_qid) — Upserts question with comment preservation
  • reorder_questions_document(files, order) — Reorders while keeping per-key comments
  • write_template_content(files, content) — Persists markdown content
  • write_config_and_template_atomically(files, content) — Transactional write with rollback on failure

4.3 Workspace and New Templates

Config:

AUTHOR:
  workspace: templates/drafts/   # Must be file://-backed, writable

sync_workspace_templates(provider, author_config) discovers template directories (those containing config.yml + template.md/template.docx) and registers them with the provider's sources_config.

create_template_scaffold() creates a new template directory with minimal config.yml (status: draft) and either a blank template.md or a copied template.docx.

5. QUESTION EDITING

5.1 Schema Validation

Location: vibe/author/services/question_schema.py

normalize_question_form(form_data, existing_ids, current_id) validates and normalizes author input:

  • Label required, question_id auto-generated from label via slugify if not provided
  • ID uniqueness enforced (auto-suffixes _2, _3 on collision)
  • Type-specific field extraction (options for choice types, min/max for number, currencies for amount, modes for period, etc.)
  • Raises QuestionSchemaError with user-friendly messages

Editable types: amount, bool, date, multichoice, number, period, radio, select, text, textarea, tristate. Advanced types (list, structured, computable, assisted) are displayed read-only.

5.2 Question CRUD Routes

All question mutations go through htmx and return HTML fragments. The question list supports drag-and-drop reordering (HTML5 drag API client-side, PUT to /questions/reorder server-side).

Auto-save: inline edit forms use hx-trigger="change" — every field change immediately persists via PUT. The response includes OOB swaps to update the editor pane, interview preview, and validation panel simultaneously.

6. SELECTION-BASED TRANSFORMS

6.1 Transform Service

Location: vibe/author/services/transform.py

Two transforms convert plain text into Jinja constructs:

Condition Wrap: apply_condition_wrap(content, start, end, *, question_id, label, default)

  • Wraps selection in {% if question_id %}...{% endif %}
  • Detects line-boundary selections for proper newline handling
  • Optionally creates bool question, or reuses an existing bool/tristate question

Interpolation: apply_interpolation(content, start, end, *, question_id, label, question_type, default, replace_all)

  • Replaces selection with {{ question_id }}
  • replace_all=True replaces all occurrences outside Jinja tags via _replace_all_outside_jinja()
  • Creates typed question with label and optional default

6.2 Safety Checks

  1. Selection validation (_validate_selection) — Rejects empty selections, out-of-bounds offsets, and selections that cross Jinja tag boundaries ({{ }}, {% %}, {# #})
  2. Jinja AST validation (_validate_jinja_result) — Parses template before and after transform; rejects if transform introduces syntax errors (but allows transforms on already-broken templates)
  3. ID uniqueness — Auto-generated IDs are checked against existing questions

6.3 Transform Dialog

Client-side dialog (author.js::openTransformDialog) captures textarea selection offsets, populates hidden fields, and auto-generates question ID from label on blur. For DOCX templates, selection mapping goes through the projection block system (section 7).

7. DOCX PROJECTION

7.1 Projection Model

Location: vibe/author/services/docx_projection.py

DOCX templates cannot be edited as raw XML. The projection system provides a lossless plain-text view:

DOCX binary → lxml parse → paragraph walk → DocxProjection(text, blocks[])
                                          Author edits text
                                          apply_docx_projection_edit()
                                          Map edits back to OOXML → rebuild zip

Each paragraph becomes a DocxProjectionBlock with:

  • block_id — Stable identifier for mapping back
  • kind"paragraph" or "table"
  • text — Projected plain text
  • editable — Whether the block's OOXML structure is simple enough for safe editing

7.2 Editability Detection

Paragraphs are marked editable only if they contain safe OOXML children (text runs with formatting). Complex elements (bookmarks, comments, fields, drawings, hyperlinks, SDT, etc.) are in RISKY_DESCENDANTS and cause the block to be marked read-only. This prevents accidental loss of formatting or structure.

7.3 DOCX Transforms

apply_docx_condition_wrap() and apply_docx_interpolation() work by:

  1. Building the projection from current DOCX bytes
  2. Applying the text-level transform on the projection
  3. Mapping the result back to OOXML paragraphs
  4. Rebuilding the DOCX zip

7.4 DOCX Preview

render_docx_as_html(docx_bytes) uses mammoth to convert DOCX to clean HTML for display when the embedded interview preview is not available.

8. ROUTE MAP

Location: vibe/author/web/routes.py

All routes require @author_required. Mutating endpoints return HTML fragments for htmx.

Template Management

Route Method Purpose
/new GET New template form
/new POST Create scaffold in workspace, redirect to workspace
/<id>/ GET Render workspace

Question CRUD

Route Method Purpose
/<id>/questions GET Question list partial
/<id>/questions/add POST Create blank question
/<id>/questions/<qid> GET Question card (view mode)
/<id>/questions/<qid>/edit GET Question card (edit mode)
/<id>/questions/<qid> PUT Auto-save inline edit
/<id>/questions/<qid> DELETE Delete question
/<id>/questions/reorder PUT Reorder via drag-drop

Editor

Route Method Purpose
/<id>/editor GET Editor pane (markdown or DOCX projection)
/<id>/content PUT Save template content

Transforms

Route Method Purpose
/<id>/transform/condition-wrap POST Apply condition-wrap
/<id>/transform/interpolate POST Apply interpolation

Interview Preview (proxied to core)

Route Method Purpose
/<id>/interview GET Start embedded interview
/<id>/interview/process POST Process form submission
/<id>/interview/reset POST Reset interview state
/<id>/interview/download GET Download rendered document

Validation

Route Method Purpose
/<id>/validate POST Run template validation, return issues

9. SESSION ARCHITECTURE

Author state is isolated from regular interview sessions:

Session Key Purpose
author_interviews.<template_id> Interview state for embedded preview (separate from main sessions)
author_preview_states.<template_id> Legacy preview state (used before embedded interview was added)

This isolation ensures authors previewing templates don't interfere with real user sessions, and vice versa.

10. DATA FLOW: QUESTION EDIT

1. User changes field in question card form
2. hx-trigger="change" → PUT /<id>/questions/<qid>
3. Server: normalize_question_form() validates
4. Server: write_question_document() persists to config.yml (ruamel.yaml round-trip)
5. Server: load_template_source(refresh=True) reloads from disk
6. Response: updated question_card.html + OOB swaps for editor + interview + validation

11. DATA FLOW: TRANSFORM

1. User selects text in editor, clicks transform button
2. JS: openTransformDialog() captures selection offsets, opens <dialog>
3. User fills label (question ID auto-generated on blur), submits
4. POST /<id>/transform/condition-wrap or /interpolate
5. Server: normalize_transform_form() validates
6. Server: apply_condition_wrap() or apply_interpolation()
   - _validate_selection() checks boundaries
   - Transform applied to content string
   - _validate_jinja_result() checks AST integrity
7. Server: write_question_document() + write_template_content() persist atomically
8. Response: transform_mutation.html with HX-Retarget to #template-source-pane
9. Editor pane re-renders with updated content, question list updates via OOB