Component System Architecture¶
Architectural reference for VIBE's component system. Optimized for LLM consumption.
See also:
core.md- Core VIBE engineassistant.md- AI-assisted interview systemreview.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:
- Try global
COMPONENT_SOURCESdirectories (filesystem) - Try relative to host template (filesystem: parent dirs, git: same commit, embedded: embedded data)
- Prioritize:
foo.md>foo/template.md>collection/foo.md - Detect collections via
collection.ymlordefaults.ymlpresence
Result: ComponentSource(virtual_path, component_type, collection_virtual_path)
3.2 Component Loading Pipeline¶
Location: vibe/templates_mgmt/provider_core.py::_load_component
- Locate via VFS
- Load config file (
component.ymlfor configured,collection.yml/defaults.ymlfor collections) - Resolve
include:directives (YAML include inheritance, recursive with cycle detection) - Resolve
uses:definition (recursive, circular detection) -- component-level - Merge definition fields with component inputs (validate no type conflicts)
- Parse template to Jinja AST
- Run static analysis (find nested
insert()calls, extract_ask,_after,_follows,_multichoiceparameters) - Recursively load nested components
- Create
ComponentTemplateData, cache inloaded_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:
- Init --
_initialize_component_callretrieves the component fromloaded_components_mapand createsComponentSetup - Pre-init --
operation.pre_init()runs operation-specific setup (e.g.InsertOperationfinds the component instance;AppendixOperationregisters the alias with probe runtime) - Validate --
_validate_component_contextchecks the component is valid - Ask-gate --
_check_ask_gateskips the component if gated off; returnsoperation.build_skip_result() - Cache check -- Looks up
(component_id, alias, phase, version_identifier)in insert cache; on hit, replays side effects and returnsoperation.build_result(cached_output) - Side-effect recorder --
_install_side_effect_recorderwrapssetup.phasewith a recordingProbePhasethat captures probe callbacks for future cache replay - Prepare context --
_prepare_component_contextbuilds the render context (7-step precedence, see 4.3) - Phase dispatch -- Inside a
contribution_scope(alias), callsoperation.execute_probe()oroperation.execute_render()depending onsetup.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() }}:
- Host probe executes template
insert()builds component context- If component needs missing values -> raises
ProbeNeedsError(missing_variables, placeholders, source_context) - Host probe catches error, merges placeholders into
probe_context - Translates component var names to host namespace ("contact_name" -> "primary.contact_name")
- Adds to
needed_vars, sets placeholders for host questions - Propagates access locations from component to host via
record_accesscallback - 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:
- Resolve manual
_insert_questions_{alias}config markers - Process
AfterQuestionplacements -- insert after targets intemplate.order - Process
FollowupOfplacements -- register intemplate.followup_placements - Process
Standaloneplacements -- append totemplate.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_onlycomponents: stored intemplate.session_context_definitions[alias]for session context validation - Questions from any component: merged into the host template's
questionsdict 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.