for-ais-only/render_hook_docs/DECISION_TREE_IMPLEMENTATION_NOTES.md
This document captures insights from implementing the decision tree render hook that go beyond the standard lessons.
Problem: Extracting metadata from nested YAML structures in Hugo templates is tricky because simple field matching returns nested occurrences.
Example: When extracting id: from a decision tree YAML, you might get:
id: documents-tree ✅id: jsonOutcome ❌ (from outcome objects)Solution: Use indentation detection to distinguish levels:
{{- if and (gt (len .) 0) (ne (index . 0) 32) (ne (index . 0) 9) -}}
{{- /* Process only top-level lines (no leading space/tab) */ -}}
{{- end -}}
Key Insight: Character codes matter - 32 = space, 9 = tab. This is more reliable than string operations.
Problem: strings.TrimPrefix doesn't always work as expected in Hugo templates.
Solution: Use strings.Replace with a count parameter instead:
{{- $afterPrefix := strings.Replace $trimmed "id:" "" 1 -}}
{{- $id = strings.TrimSpace $afterPrefix -}}
Why: Replace is more predictable and handles edge cases better than TrimPrefix.
Key Principle: Metadata for AI agents MUST be in static HTML, not JavaScript.
Why: AI agents typically don't execute JavaScript. If metadata is only created by JavaScript, AI agents won't see it.
Implementation:
<script type="application/json"> in HTMLPattern: Add scope field to help AI agents understand component purpose.
Benefits:
Example:
id: documents-tree
scope: documents
Three-Layer Design:
<pre>, ready for AI agentsBenefit: Each layer works independently. AI agents get metadata without JS. Humans get interactive diagrams. Non-JS users get raw content.
Lesson: Never hardcode SVG dimensions. Calculate dynamically based on content.
Common Mistake:
const svgWidth = leftMargin + (maxDepth + 1) * indentWidth + 320; // ❌ Hardcoded
Better:
const svgWidth = leftMargin + (maxDepth + 1) * indentWidth + maxBoxWidth + 40;
Why: Content varies. Hardcoded values cause cutoff issues when content is larger than expected.
Pattern: Calculate max characters per line based on font metrics:
const charWidth = 8; // Space Mono at 14px
const maxCharsPerLine = Math.floor(maxBoxWidth / charWidth);
Considerations:
Pattern: Use info string attributes to pass metadata to render hook:
```decision-tree {id="documents-tree"}
Access in render hook:
{{- $id := .Attributes.id -}}
Lesson: Keep fence attributes simple. Complex data should go in the YAML body.
Critical: Always test with 3+ instances on the same page.
Why:
Result: Discovered that multiple trees on one page work correctly with proper ID handling.
Lesson: Comprehensive documentation is not optional—it's part of making the component usable.
Should Document:
Benefit: Helps future implementers and enables AI agents to understand the format.
Discovery: Not all decision trees are "selection trees" (choose between options). Some are "suitability trees" (determine if something is appropriate).
Problem: Selection trees and suitability trees have fundamentally different semantics:
Solution: Add optional sentiment field to outcomes:
outcome:
label: "✅ RDI is a good fit for your use case"
id: goodFit
sentiment: "positive" # Green styling
Implementation Details:
sentiment field during YAML parsing in JavaScriptsentiment: "positive" → Green background (#0fa869) and bordersentiment: "negative" → Red background (#d9534f) and bordersentiment: "indeterminate" → Yellow/amber background (#f0ad4e) and borderKey Insight: Explicit metadata is better than heuristics. Don't try to infer sentiment from emoji (✅/❌) or label text. Use explicit fields for reliability and AI agent compatibility.
Backward Compatibility: Existing trees without sentiment fields continue to work with default red styling. This allows gradual adoption.
Discovery: The JavaScript had two issues preventing YAML answer order from being respected:
flattenDecisionTree() function was hardcoded to process "yes" first, then "no"Problem: This prevented authors from controlling the visual layout of the tree. If you wanted "no" outcomes to appear first (for early rejection patterns), the diagram would still show "yes" first.
Solution:
flattenDecisionTree() to iterate through answer keys in the order they appear in the YAMLdrawTreeLines() to use the actual answer value stored in each item instead of deriving it from position// In flattenDecisionTree():
const answerKeys = Object.keys(question.answers);
answerKeys.forEach(answerKey => {
const answer = question.answers[answerKey];
// Process in order, storing answer.value in the item
});
// In drawTreeLines():
answerLabel = item.answer || 'Yes'; // Use stored value, not position
Benefit: Authors can now control tree layout by ordering answers in the YAML:
no first for early rejection patterns (negative outcomes appear left)yes first for positive-path-first patterns (positive outcomes appear left)Key Insight: YAML object key order is preserved in JavaScript (since ES2015), and we now respect both the order AND the actual answer values, making the layout fully author-controlled.
Problem: Deeply nested decision trees (with many levels of questions) can become too wide to fit on the page, requiring horizontal scrolling.
Solution: Added optional indentWidth parameter to the YAML root object that controls the horizontal spacing between parent and child nodes:
id: when-to-use-rdi
scope: rdi
indentWidth: 25 # Reduce from default 40 to make tree narrower
rootQuestion: cacheTarget
questions:
# ...
Implementation:
In renderDecisionTree(), the indent width is read from treeData.indentWidth with a sensible default:
const indentWidth = treeData.indentWidth ? parseInt(treeData.indentWidth) : 40;
Design Rationale: While indentWidth is a rendering preference, it's included in the YAML because:
Benefit: Authors can now control tree width by adjusting indentWidth:
leftMargin + (maxDepth + 1) * indentWidth + maxBoxWidth + 40Recommendation: For trees with 8+ levels of nesting, try indentWidth: 25 or lower to keep the diagram readable without horizontal scrolling.
Problem: When using reduced indentWidth values, the Yes/No labels on the connecting lines were being covered by the node boxes they referred to.
Solution:
y + 10 to y + 16 pixelsImplementation:
const labelY = y + 16; // Increased offset
// Add white background rectangle behind label
const labelBg = document.createElementNS('http://www.w3.org/2000/svg', 'rect');
labelBg.setAttribute('x', labelX - 12);
labelBg.setAttribute('y', labelY - 9);
labelBg.setAttribute('width', '24');
labelBg.setAttribute('height', '12');
labelBg.setAttribute('fill', 'white');
svg.appendChild(labelBg);
Benefit: Labels remain readable regardless of indent width or tree density.