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 systemdesign-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:
2.2 Authorization¶
Location: vibe/author/auth.py
is_author() -> bool— Checks session grants for"author"(from PASETO tokenvibe_grants) ORdev_settings.authorflag@author_required— Route decorator, aborts 403 if not authoris_authorregistered 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 bindsg.interview,g.template_data,g.version_url_adapterfor the request, then restores previous values_author_version_url_for()remaps core interview endpoints to author namespace (e.g.,interview.process_htmx→author.author_interview_process)X-Frame-Options: SAMEORIGINset for author interview routes invibe/core.py(all other routes useDENY)- 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
- Calls
provider.get_template_data()for full validation - 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 - Returns
TemplateSourcedataclass 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()→EditableTemplateFileswith round-trip YAML document (ruamel.yamltyp="rt")write_config_document(files)— Serializes YAML preserving commentswrite_question_document(files, qid, question, previous_qid)— Upserts question with comment preservationreorder_questions_document(files, order)— Reorders while keeping per-key commentswrite_template_content(files, content)— Persists markdown contentwrite_config_and_template_atomically(files, content)— Transactional write with rollback on failure
4.3 Workspace and New Templates¶
Config:
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,_3on collision) - Type-specific field extraction (options for choice types, min/max for number, currencies for amount, modes for period, etc.)
- Raises
QuestionSchemaErrorwith 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=Truereplaces all occurrences outside Jinja tags via_replace_all_outside_jinja()- Creates typed question with label and optional default
6.2 Safety Checks¶
- Selection validation (
_validate_selection) — Rejects empty selections, out-of-bounds offsets, and selections that cross Jinja tag boundaries ({{ }},{% %},{# #}) - 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) - 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 backkind—"paragraph"or"table"text— Projected plain texteditable— 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:
- Building the projection from current DOCX bytes
- Applying the text-level transform on the projection
- Mapping the result back to OOXML paragraphs
- 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