Skip to content

Component System Architecture

Architectural reference for VIBE's component system. Optimized for LLM consumption.

See also:

  • core.md - Core VIBE engine
  • assistant.md - AI-assisted interview system
  • review.md - Document compliance review

1. SYSTEM OVERVIEW

Components are modular, reusable template sections inserted via {{ insert() }}. Four progressive complexity tiers:

File-Drop -- Single .md/.docx file, no config. Full host passthrough (shared namespace). has_own_questions = False.

Configured -- Directory with component.yml. Formal inputs and/or questions. Components with questions get selective passthrough (isolated namespace); components with only inputs get full passthrough.

Definition-Linked -- Uses uses: definition_name, inherits structure from definitions. Has own questions only if the component or its resolved definition contributes entries to questions (checked via has_own_questions, which is bool(self.questions)).

Collection -- Multiple components grouped under a directory with collection.yml (or legacy defaults.yml), shared collection-level questions.

Inline -- Defined inside a host template via {% component "name" %}...{% endcomponent %}. Parsed by InlineComponentExtension, extracted during AST walk, compiled per-consumer. Can be reused by other templates via insert().

Key Features:

  • Multi-instance support with aliases (each instance = separate namespace)
  • Data precedence (highest to lowest): insert() mappings -> host component_defaults -> component input defaults -> host passthrough
  • Component-specific questions injected into host interview with placement control (_after, _follows, _multichoice)
  • Recursive component loading (components can insert components)
  • Config inheritance via include: directive (YAML include)
  • Cross-cutting contribution routing ({% contribute %} / {{ insert_contributions() }})

2. COMPONENT TYPES & LEVELS

2.1 Level 0: File-Drop Components

components/signature_block.md     # Standalone
components/legal_clauses/          # Collection
    |- collection.yml              # Preferred (defaults.yml also accepted)
    |- force_majeure.md
    \- confidentiality.md

No component.yml, sees all host variables (shared namespace), inherits from host component_defaults.

2.2 Level 1: Configured Components

# component.yml
inputs:
  contact_name: { type: text, required: true }
  contact_email: { type: text }
questions:
  add_phone: { type: bool, label: "Include phone?" }
cacheable: true   # default; set to false to opt out of insert-level memoization

Isolated namespace, only sees declared inputs. Component questions appear in host interview at _insert_questions_{alias} marker position.

2.3 Level 2: Definition-Linked Components

# component.yml
uses: contact_definition      # Inherits all definition fields as inputs
inputs:
  department: { type: text }  # Additional inputs

Each alias creates isolated namespace with namespaced questions: primary.full_name (from definition), primary.department (from component). Multiple instances maintain independent data.

2.4 Inline Components

{# Defined inside a host template #}
{% component "disclaimer" %}
  {{ disclaimer_text }}
{% endcomponent %}

Parsed by vibe/jinja/component_ext.py::InlineComponentExtension. The raw source is stored on ComponentTemplateData.inline_source and compiled per-consumer during registration. Supports autoinsert=false to suppress automatic insertion.

3. COMPONENT DISCOVERY & LOADING

3.1 Virtual File System (VFS)

Location: vibe/templates_mgmt/provider_vfs.py::VirtualFileSystem

URI Schemes: file://, git://<base64_repo>/<commit>/<path>, embedded://<template_id>/<path>

Discovery Algorithm:

  1. Try global COMPONENT_SOURCES directories (filesystem)
  2. Try relative to host template (filesystem: parent dirs, git: same commit, embedded: embedded data)
  3. Prioritize: foo.md > foo/template.md > collection/foo.md
  4. Detect collections via collection.yml or defaults.yml presence

Result: ComponentSource(virtual_path, component_type, collection_virtual_path)

3.2 Component Loading Pipeline

Location: vibe/templates_mgmt/provider_core.py::_load_component

  1. Locate via VFS
  2. Load config file (component.yml for configured, collection.yml/defaults.yml for collections)
  3. Resolve include: directives (YAML include inheritance, recursive with cycle detection)
  4. Resolve uses: definition (recursive, circular detection) -- component-level
  5. Merge definition fields with component inputs (validate no type conflicts)
  6. Parse template to Jinja AST
  7. Run static analysis (find nested insert() calls, extract _ask, _after, _follows, _multichoice parameters)
  8. Recursively load nested components
  9. Create ComponentTemplateData, cache in loaded_components_map

Key Structure: ComponentTemplateData dataclass (vibe/structures.py) with fields: component_id, template_obj, questions, inputs, outputs, uses_definition, loaded_components_map, organization (ComponentOrganization), component_defaults, is_appendix, definition_only, encapsulation, collection_questions, collection_id, template_referenced_vars, inline_source. Property component_type is computed from organization. Property has_own_questions returns bool(self.questions).

4. COMPONENT INSERTION & RENDERING

4.1 Static Analysis Phase

Location: vibe/static_analysis/visitors_extraction.py::InsertComponentVisitor

During template loading, finds all {{ insert() }} calls, extracts component_id, alias, mappings (as AST nodes), stores in template.component_instances_ast, triggers component loading.

4.2 Runtime Insertion -- Unified Execution Pipeline

Location: vibe/component/__init__.py::_execute_component (shared pipeline), insert and appendix (thin wrappers)

Both insert() and appendix() extract their control parameters (_alias, _ask, etc.) and delegate to _execute_component with an operation type object (InsertOperation or AppendixOperation). The operation type (vibe/component/operations.py::ComponentOperation ABC) provides hooks for operation-specific behavior at each pipeline step.

Pipeline steps:

  1. Init -- _initialize_component_call retrieves the component from loaded_components_map and creates ComponentSetup
  2. Pre-init -- operation.pre_init() runs operation-specific setup (e.g. InsertOperation finds the component instance; AppendixOperation registers the alias with probe runtime)
  3. Validate -- _validate_component_context checks the component is valid
  4. Ask-gate -- _check_ask_gate skips the component if gated off; returns operation.build_skip_result()
  5. Cache check -- Looks up (component_id, alias, phase, version_identifier) in insert cache; on hit, replays side effects and returns operation.build_result(cached_output)
  6. Side-effect recorder -- _install_side_effect_recorder wraps setup.phase with a recording ProbePhase that captures probe callbacks for future cache replay
  7. Prepare context -- _prepare_component_context builds the render context (7-step precedence, see 4.3)
  8. Phase dispatch -- Inside a contribution_scope(alias), calls operation.execute_probe() or operation.execute_render() depending on setup.is_probing. The contribution scope tags any {% contribute %} output with the current component alias for hierarchical scoping.

Phase protocol: ComponentSetup has a phase: Phase field. ProbePhase wraps real probe runtime callbacks; RenderPhase provides no-ops. All pipeline code calls setup.phase.mark_needed(), setup.phase.merge_placeholders(), etc. The side-effect recorder works by replacing setup.phase with a recording wrapper.

Operation hooks on ComponentOperation:

Hook Purpose
namespace_prefix Namespace for questions (_components for insert, _appendices for appendix)
pre_init Operation-specific setup before validation
execute_probe Run the probing phase
execute_render Run the rendering phase
on_cache_hit Extra work on cache hit (e.g. appendix manager writes)
on_probe_complete Extra work after probe (e.g. appendix manager writes)
build_result Construct return value from rendered content
build_skip_result Return value when ask-gate skips the component
on_invalid_component Fallback result for invalid components
cache_phase_str Phase string for cache key construction

4.3 Component Context Building

Location: vibe/component/mapping.py::_prepare_component_context, returns ContextResult with context, provenance, passthrough_vars, and cache metadata.

Seven steps in order, tracked by an added_vars set so that earlier (higher-precedence) values are never overwritten:

Step Helper What it provides
1 _apply_explicit_param_mappings Explicit insert()/appendix() kwargs -- highest precedence
2 _apply_host_component_defaults Host component_defaults: values (Jinja strings rendered with host state)
3 _apply_component_defaults Component inputs: defaults (for non-required inputs without a value yet)
4 _apply_host_passthrough_level0/1plus Host passthrough -- branched on component.has_own_questions
5 _map_definition_inputs_from_host Definition inputs from namespaced host questions (definition-linked components)
6 _map_questions_from_host_state Component questions from host state (e.g. _components.alias.q -> q)
7 _map_questions_from_host_state Collection-level questions from host state

Step 4 only runs for components without encapsulation: strict. Session context variables always override added_vars because they are real external data. Passthrough branching based on component.has_own_questions: without own questions = full passthrough with placeholders; with own questions = selective passthrough (only host variables with values).

5. COMPONENT PROBING

5.1 Probing Flow

Location: vibe/probing/orchestrator.py::run_probe

When template probe encounters {{ insert() }}:

  1. Host probe executes template
  2. insert() builds component context
  3. If component needs missing values -> raises ProbeNeedsError(missing_variables, placeholders, source_context)
  4. Host probe catches error, merges placeholders into probe_context
  5. Translates component var names to host namespace ("contact_name" -> "primary.contact_name")
  6. Adds to needed_vars, sets placeholders for host questions
  7. Propagates access locations from component to host via record_access callback
  8. Retries probe with expanded context

The Phase protocol decouples component code from raw callback dicts, and enables the side-effect recorder to transparently wrap the phase for cache replay.

5.2 Multi-Level Probing

Nested components cascade errors up with namespace translation:

  • Component B raises: ProbeNeedsError(["field_x"], source_context={"alias":"sub"})
  • Component A translates, re-raises: ProbeNeedsError(["sub.field_x"], ...)
  • Host translates: needed_vars.append("main.sub.field_x")

6. COMPONENT QUESTIONS & NAMESPACING

6.1 Question Placement System

Location: vibe/templates_mgmt/question_placement.py

Component questions are placed into the host interview using a declarative two-phase system:

Collection phase (collect_placements): Each component instance produces QuestionPlacement records based on its questions and the _after/_follows/_multichoice/_ask parameters from its insert() call.

Resolution phase (resolve_placements): A single pass processes all QuestionPlacement records:

  1. Resolve manual _insert_questions_{alias} config markers
  2. Process AfterQuestion placements -- insert after targets in template.order
  3. Process FollowupOf placements -- register in template.followup_placements
  4. Process Standalone placements -- append to template.order

Each placement has a PlacementSpec (one of AfterQuestion, FollowupOf, InMultichoice, Standalone) that declares its positioning intent. Invalid placements fall back to Standalone.

6.2 Placement Directives

_after="question_id" -- Place component questions after a host question in the interview order.

_follows="question_id" -- Nest component questions as followups inside a host question's widget. NOT added to template.order.

_ask="Label text" -- Generate an auto-gate question (bool) that controls whether the component is included. Gate question ID is include_{alias}.

_multichoice="Group Label" -- Group multiple insert() calls under a single multichoice question. Each member must also have _ask= (becomes a checkbox option). All members must agree on _after=; _follows= is incompatible.

6.3 Collection Question Followup Nesting

When collection components are gated by a multichoice question and a collection-level question is referenced by exactly one component's template, that question is automatically nested as a followup of that component's checkbox option. When ambiguous (0 or 2+ references), falls back to standalone placement.

6.4 Namespace Isolation vs Passthrough

Access patterns differ between the host template (where insert() is called) and inside the component template (what the component's own Jinja sees). The mapping layer (vibe/component/mapping.py::_map_definition_inputs_from_host) copies namespaced host values into plain input names for the component context.

Host template perspective (how the host references component data):

Component type Namespace Host access
Without own questions Shared {{ signer_name }}
With own questions Isolated {{ bar.field }}
Definition-linked Isolated {{ primary.full_name }}
Any N/A {{ _components.bar.field }}

Inside the component template, variables are accessed by their plain input name (e.g. {{ full_name }}), regardless of the alias used in the host.

6.5 Template-Level uses: Declarations

Location: vibe/templates_mgmt/provider_core.py::_process_uses_declarations

A template's config.yml can declare uses: to reference components at the template level (distinct from the component-level uses: in component.yml that links components to definitions).

# config.yml
uses:
  - organization            # component_id = alias = "organization"
  - { ramavtal: framework } # component_id = "ramavtal", alias = "framework"

What uses: imports:

  • Definitions from definition_only components: stored in template.session_context_definitions[alias] for session context validation
  • Questions from any component: merged into the host template's questions dict in host namespace (no _components. prefix). Host question definitions take precedence -- if the same key exists in both host and component, the host's definition wins.

Contrast with insert(): A component referenced via uses: contributes questions directly to the host namespace without rendering the component's template. The same component can be insert()ed by one template (getting namespaced questions under _components.alias.* plus rendered template content) and uses:d by another (getting questions merged into host namespace, no template rendering). Imported questions become relevant through normal probing -- only those actually referenced by the host template are shown.

Warning behavior: A warning is emitted only when the uses:-referenced component has neither definition_only status nor any questions, since such a component contributes nothing via uses:.

7. CONTRIBUTION ROUTING SYSTEM

7.1 Overview

Contributions enable cross-cutting content routing: components produce content fragments during rendering (pass 1) that are collected and inserted at designated locations during resolution (pass 2). Primary use case is the definitions system, where multiple components contribute term definitions to a single glossary section.

7.2 Core Components

ContributionManager (vibe/contributions/manager.py) -- Request-scoped store. Components call manager.register(slot_name, content) to contribute. Supports hierarchical scoping via contribution_scope(alias) context manager so appendices drain only their own subtree.

{% contribute "slot_name" %}...{% endcontribute %} (vibe/jinja/contribute_ext.py::ContributeExtension) -- Jinja block tag that captures rendered body and registers it with the ContributionManager rather than emitting inline. Slot name must be a string literal for static analysis. Supports multilingual tag aliases.

{{ insert_contributions("slot_name") }} (vibe/jinja/contribute_globals.py) -- Emits a deferred HTML-comment marker during pass 1. During pass 2, resolve_markers() replaces markers with drained contributions. Optional key= parameter for sorting.

resolve_markers() (vibe/contributions/resolution.py) -- Pass-2 regex substitution replacing <!-- SLOT:name:hex12 --> markers with drained content. Each slot is drained on first marker; subsequent markers for the same slot resolve to empty.

7.3 Definitions Layer

Built on top of contributions, the definitions layer provides structured term management:

{% definition %}**Term** means...{% enddefinition %} (vibe/jinja/definition_ext.py::DefinitionExtension) -- Semantic markup tag. Inside {% contribute %}: registers term in DefinitionRegistry AND routes as individual contribution for per-term sorting. Outside {% contribute %} (inline): registers for tooltip matching only, renders in place.

{% term %}Force Majeure{% endterm %} -- Wraps term text in <span class="defined-term-ref"> for tooltip styling.

DefinitionRegistry (vibe/contributions/definitions.py) -- Request-scoped registry handling dedup (same term + body = silent), conflict detection (same term + different body = warning, first wins), and provides the term map for tooltip injection.

{{ insert_definitions() }} -- Convenience wrapper around insert_contributions("definitions") with alphabetical sort key extracted from **bold** term names.

7.4 Data Flow

Pass 1 (Jinja rendering):
  Component A: {% contribute "definitions" %}{% definition %}**Term**...{% enddefinition %}{% endcontribute %}
      |
  ContributeExtension._render_contribute()
      |
  DefinitionExtension._render_definition() → DefinitionRegistry.register() + ContributionManager.register()
      |
  Host: {{ insert_definitions() }} → generates <!-- SLOT:definitions:a1b2c3d4e5f6 -->

Pass 2 (marker resolution):
  resolve_markers(rendered_text, manager) → replaces markers with sorted, drained contributions

8. INSERT-LEVEL MEMOIZATION

Location: vibe/probing/insert_cache.py (cache infrastructure), vibe/component/cache.py (cache helpers)

Each insert() / appendix() call is cached keyed by (component_id, alias, phase, version_identifier). The cache fingerprint is computed from host-state values at the paths the component depends on. On subsequent calls where those values haven't changed, the cached result is returned and recorded side effects are replayed.

8.1 What Gets Fingerprinted

  • component_defaults references -- ALL host vars referenced by component_defaults: Jinja expressions
  • Namespaced component questions -- _components.{alias}.{question_name} paths
  • Passthrough vars (non-strict only) -- Narrowed after probe to only actually accessed vars
  • Session context vars -- Always included for non-strict components
  • Resolved explicit params -- Dynamic expressions in insert() kwargs

8.2 Side Effect Replay

During probing, SideEffectRecorder (installed by _install_side_effect_recorder in step 6) wraps setup.phase with a recording ProbePhase. On cache hit, recorded effects are replayed via replay_side_effects(). Recorder is installed before context preparation so host_dependency_paths callbacks are also captured.

8.3 Cache Lifecycle

Session-scoped. load_cache_from_session(interview) at request start; save_cache_to_session(interview) at request end. All entries must be picklable. Opt out per-component (cacheable: false in component.yml) or globally (insert_caching: false in config.yml).

9. DATA FLOW DIAGRAMS

9.1 Component Loading

HTTP GET /interview/{id}/
    |
get_template_data -> _load_template
    |
Parse AST -> InsertComponentVisitor
    |   (extracts _ask, _after, _follows, _multichoice parameters)
    |
VFS.locate_component -> _load_component
    |
Load config (component.yml / collection.yml / defaults.yml)
    |
resolve_includes (YAML include inheritance)
    |
Resolve uses: definition
    |
Parse component template -> Find nested inserts -> Recursive load
    |
Create ComponentTemplateData -> Cache in loaded_components_map
    |
Process template-level uses: declarations
    |  - Load referenced components
    |  - Import definitions into session_context_definitions
    |  - Import component questions into host questions (host wins on conflict)
    |
Question Placement:
    collect_placements(template) -> list[QuestionPlacement]
    resolve_placements(template, placements) -> writes template.questions/order/followup_placements

9.2 Component Insertion (Unified Pipeline)

{{ insert("foo", _alias="bar", x=y) }}   OR   {{ appendix("foo", "bar") }}
    |
insert()/appendix() -> _execute_component(operation=InsertOperation/AppendixOperation)
    |
1. _initialize_component_call -> ComponentSetup (with phase: ProbePhase or RenderPhase)
2. operation.pre_init (find instance / register alias)
3. _validate_component_context
4. _check_ask_gate -> skip if gated off
5. Cache check (keyed by component_id, alias, phase, version_identifier)
    Cache hit? -> replay side effects, return
    Cache miss? -> continue
6. _install_side_effect_recorder (wraps setup.phase)
7. _prepare_component_context -> ContextResult (7-step precedence)
8. contribution_scope(alias) -> Phase dispatch:
    Probing -> operation.execute_probe
    Render  -> operation.execute_render
    |
Store result in insert cache -> Return content

10. FILE LOCATION INDEX

Core Component System

What Where
Unified pipeline vibe/component/__init__.py::_execute_component
insert() wrapper vibe/component/__init__.py::insert
appendix() wrapper vibe/component/__init__.py::appendix
Operation types (ABC) vibe/component/operations.py::ComponentOperation
InsertOperation vibe/component/operations.py::InsertOperation
AppendixOperation vibe/component/operations.py::AppendixOperation
Phase protocol vibe/component/setup.py::Phase
ProbePhase / RenderPhase vibe/component/setup.py::ProbePhase
Component setup vibe/component/setup.py::ComponentSetup
Context builder vibe/component/mapping.py::_prepare_component_context
Probe execution vibe/component/probe.py::_execute_insert_probe
Side-effect recorder vibe/component/probe.py::_install_side_effect_recorder
Relevance tracking vibe/component/relevance.py::_track_component_relevance
Cache helpers vibe/component/cache.py
Appendix probe support vibe/component/appendix_support.py
Inline components ext vibe/jinja/component_ext.py::InlineComponentExtension
Insert cache vibe/probing/insert_cache.py
Component loading vibe/templates_mgmt/provider_core.py::_load_component
Uses declarations vibe/templates_mgmt/provider_core.py::_process_uses_declarations
Component discovery vibe/templates_mgmt/provider_vfs.py::locate_component

Contribution System

What Where
ContributionManager vibe/contributions/manager.py::ContributionManager
Contribution scope vibe/contributions/manager.py::contribution_scope
Marker resolution vibe/contributions/resolution.py::resolve_markers
DefinitionRegistry vibe/contributions/definitions.py::DefinitionRegistry
insert_definitions() vibe/contributions/definitions.py::insert_definitions
{% contribute %} extension vibe/jinja/contribute_ext.py::ContributeExtension
{% definition %} extension vibe/jinja/definition_ext.py::DefinitionExtension
insert_contributions() global vibe/jinja/contribute_globals.py::insert_contributions

Data Structures

What Where
ComponentTemplateData vibe/structures.py::ComponentTemplateData
ComponentSource vibe/templates_mgmt/provider_types.py::ComponentSource
ComponentInstanceArgs vibe/structures.py::ComponentInstanceArgs

Question Placement

What Where
Placement collection vibe/templates_mgmt/question_placement.py::collect_placements
Placement resolution vibe/templates_mgmt/question_placement.py::resolve_placements
Definition question collection vibe/templates_mgmt/question_placement.py::_collect_definition_question_placements
Definition resolution vibe/templates_mgmt/provider_core.py::resolve_definition
Static analysis vibe/static_analysis/visitors_extraction.py::InsertComponentVisitor

Document Version: 7.0

Last Updated: 2026-04-06

Notes: Added Section 6.5 documenting template-level uses: declarations and their ability to import questions from regular components (not just definitions from definition_only components) into the host namespace. Updated data flow diagram (Section 9.1) and file location index (Section 10) accordingly.