Skip to content

feat: preserve inline object type in template resolution (fixes #31)#32

Merged
spinje merged 2 commits into
mainfrom
feat/inline-object-type
Jan 1, 2026
Merged

feat: preserve inline object type in template resolution (fixes #31)#32
spinje merged 2 commits into
mainfrom
feat/inline-object-type

Conversation

@spinje
Copy link
Copy Markdown
Owner

@spinje spinje commented Jan 1, 2026

Summary

Fixes the double-serialization bug where templates in nested structures like {"key": "${dict_var}"} would produce {"key": "{\"nested\": \"value\"}"} instead of the correct {"key": {"nested": "value"}}.

This enables the intuitive pattern of passing multiple data sources to shell commands:

{
  "stdin": {"config": "${config}", "data": "${data}"},
  "command": "jq '.config + .data'"
}

Changes

  • New helper methods in TemplateResolver:

    • is_simple_template() - detects if string is exactly ${var}
    • extract_simple_template_var() - extracts variable name from simple template
  • Renamed resolve_string()resolve_template() with type preservation:

    • Simple templates (${var}) preserve original type (dict, list, int, bool, None)
    • Complex templates ("Hello ${name}") return interpolated strings
  • Updated callers:

    • workflow_executor.py - nested workflow param mapping
    • node_wrapper.py - refactored to use shared helper (DRY)

Test Coverage

  • test_template_type_preservation.py - Core type preservation behavior (12 tests)
  • test_shell_stdin_type_preservation.py - Integration tests with shell node (4 tests)
  • Updated existing tests to use resolve_template() (36 occurrences across 5 files)

Created Docs

  • .taskmaster/tasks/task_103/implementation/implementation-plan.md
  • .taskmaster/tasks/task_103/implementation/progress-log.md

File Stats

 13 files changed, 1620 insertions(+), 98 deletions(-)

Testing

make test  # 3236 passed, 9 skipped

Task-ID: 103

spinje added 2 commits January 1, 2026 12:00
Fixes the double-serialization bug where templates in nested structures
like {"key": "${dict_var}"} would produce {"key": "{\"nested\": \"value\"}"}
instead of the correct {"key": {"nested": "value"}}.

Changes:
- Add is_simple_template() and extract_simple_template_var() helpers
- Rename resolve_string() to resolve_template() with type preservation
- Simple templates (${var}) now preserve original type (dict, list, int, etc.)
- Complex templates ("Hello ${name}") still return interpolated strings
- Update node_wrapper to use shared helper for DRY
- Update workflow_executor to use resolve_template

Fixes #31

Task-ID: 103
PR review fixes:
- Extract shared _VAR_NAME_PATTERN for consistency between TEMPLATE_PATTERN
  and SIMPLE_TEMPLATE_PATTERN
- Stricter validation rejects invalid variable names (${123}, ${ var })
- Add test for invalid variable name patterns

Documentation:
- Update template-variables.md with type preservation behavior
- Update runtime CLAUDE.md with new method names
- Add comprehensive task-103 review document
@claude
Copy link
Copy Markdown

claude Bot commented Jan 1, 2026

Code Review: Template Type Preservation (PR #32)

Summary

This PR fixes a double-serialization bug where simple templates in nested structures (e.g., {"key": "${dict_var}"}) were incorrectly JSON-serialized to strings instead of preserving their original types. The implementation is clean, well-tested, and follows the project's established patterns.


Critical — No blocking issues found

The implementation is production-ready with no critical issues.


⚠️ Warnings — Should be addressed

1. SIMPLE_TEMPLATE_PATTERN is too permissive (Medium Priority)

Location: src/pflow/runtime/template_resolver.py:32

Issue:

SIMPLE_TEMPLATE_PATTERN = re.compile(r"^\$\{([^}]+)\}$")

This pattern uses [^}]+ which matches any character except }, including:

  • Invalid variable names: ${123}, ${-invalid}, ${$weird}
  • Special characters: ${@#$%}
  • Spaces: ${ var } (space-padded)

Why it matters:

  1. Inconsistency: TEMPLATE_PATTERN on line 26-28 is strict and only matches valid identifiers
  2. False positives: Invalid templates will be treated as simple templates and attempt resolution
  3. Debugging confusion: ${123} would be treated as simple template but fail to resolve

Recommended fix:
Reuse the variable name pattern from TEMPLATE_PATTERN:

# Current strict pattern for valid variable names
TEMPLATE_PATTERN = re.compile(
    r"(?<!\$)\$\{([a-zA-Z_][\w-]*(?:(?:\[\d+\])?(?:\.[a-zA-Z_][\w-]*(?:\[\d+\])?)*)?)\}"
)

# Make SIMPLE_TEMPLATE_PATTERN match the same valid names
SIMPLE_TEMPLATE_PATTERN = re.compile(
    r"^\$\{([a-zA-Z_][\w-]*(?:(?:\[\d+\])?(?:\.[a-zA-Z_][\w-]*(?:\[\d+\])?)*)?)\}$"
)

Why not critical: In practice, the resolve_value() call will fail gracefully for invalid variable names, so this won't cause runtime crashes. But it's sloppy and violates the principle of "fail fast with clear errors."


2. Missing edge case test: whitespace-padded templates (Low Priority)

Location: tests/test_runtime/test_template_type_preservation.py

Issue:
The current SIMPLE_TEMPLATE_PATTERN would match "${ var }" (with spaces), but this is likely not intended behavior.

Recommended test addition:

def test_whitespace_padded_template_not_simple(self):
    """Templates with whitespace padding should not be treated as simple."""
    # These should be rejected as simple templates
    assert TemplateResolver.is_simple_template("${ var }") is False
    assert TemplateResolver.is_simple_template("${var }") is False
    assert TemplateResolver.is_simple_template("${ var}") is False

If the stricter regex from Warning #1 is adopted, this test will pass automatically.


3. Documentation: Type preservation behavior not documented in CLAUDE.md (Low Priority)

Location: architecture/reference/template-variables.md or src/pflow/runtime/CLAUDE.md

Issue:
This is a significant behavior change that affects all nodes accepting structured parameters. The architecture docs should document:

  1. When types are preserved vs. stringified
  2. The simple vs. complex template distinction
  3. Examples showing the difference

Recommended addition to architecture/reference/template-variables.md:

## Type Preservation (v0.6.0+)

### Simple Templates
Templates that consist of **only** a variable reference preserve the original type:

\`\`\`json
{
  "data": "${user_object}"  // ← Dict stays dict
}
\`\`\`

### Complex Templates
Templates with text around variables always return strings:

\`\`\`json
{
  "message": "Hello ${name}!"  // ← Always string
}
\`\`\`

### Examples

**Before (v0.5.x):**
\`\`\`python
{"config": "${settings}"}  # → {"config": "{\"key\": \"val\"}"}  ❌
\`\`\`

**After (v0.6.0+):**
\`\`\`python
{"config": "${settings}"}  # → {"config": {"key": "val"}}  ✅
\`\`\`

💡 Suggestions — Optional improvements

1. Consider extracting SIMPLE_VAR_PATTERN as shared constant (Code organization)

Location: src/pflow/runtime/template_resolver.py

Current state:
Both TEMPLATE_PATTERN and SIMPLE_TEMPLATE_PATTERN duplicate the variable name regex.

Suggestion:

# Shared pattern for valid variable names with paths and array indices
_VAR_NAME_PATTERN = r"[a-zA-Z_][\w-]*(?:(?:\[\d+\])?(?:\.[a-zA-Z_][\w-]*(?:\[\d+\])?)*)?"

TEMPLATE_PATTERN = re.compile(rf"(?<!\$)\$\{({_VAR_NAME_PATTERN})\}")
SIMPLE_TEMPLATE_PATTERN = re.compile(rf"^\$\{({_VAR_NAME_PATTERN})\}$")

Benefits:

  • Single source of truth for "what is a valid variable name"
  • Easier to maintain if pattern needs updates
  • Self-documenting

2. Test coverage: Verify behavior with escaped templates (Test quality)

Location: tests/test_runtime/test_template_type_preservation.py

Missing test case:
What happens with $$$\{var} (escaped template)?

Suggested test:

def test_escaped_template_not_treated_as_simple(self):
    """Escaped templates should not be detected as simple templates."""
    assert TemplateResolver.is_simple_template("$$$\{var}") is False

    context = {"var": {"data": "value"}}
    result = TemplateResolver.resolve_template("$$$\{var}", context)
    # Should return literal string "$$\{var}", not the resolved dict
    assert result == "$$\{var}"

3. Minor: Logging could include context size for debugging (Observability)

Location: src/pflow/runtime/template_resolver.py:369-372

Current:

logger.debug(
    f"Resolved simple template '$\{{var_name}}' -> {resolved!r} (type: {type(resolved).__name__})",
    extra={"var_name": var_name, "value_type": type(resolved).__name__},
)

Suggestion:

logger.debug(
    f"Resolved simple template '$\{{var_name}}' -> {resolved!r} (type: {type(resolved).__name__})",
    extra={
        "var_name": var_name,
        "value_type": type(resolved).__name__,
        "context_keys": list(context.keys())  # ← Helpful for debugging missing vars
    },
)

This helps when debugging "why isn't my variable resolving?" issues.


🎯 Overall Assessment

Code Quality: 8.5/10

  • ✅ Clean separation of concerns (helpers extracted properly)
  • ✅ Excellent test coverage (16 test files updated, 2 new test files)
  • ✅ Clear naming (is_simple_template, extract_simple_template_var)
  • ✅ Good documentation in docstrings
  • ⚠️ Regex pattern could be stricter (Warning docs: Set up Mintlify documentation (Task 93) #1)

Architecture: 9/10

  • ✅ Follows established patterns (TemplateResolver already had this structure)
  • ✅ DRY: Eliminated duplication with node_wrapper._resolve_simple_template
  • ✅ Backward compatible: All existing complex templates still work
  • ✅ Single responsibility: Each method does one thing well

Test Quality: 9/10

  • ✅ Tests document the why (bug fix scenario) not just what
  • ✅ Integration tests verify real-world use case (shell + jq)
  • ✅ Edge cases covered (None, bool, int, deeply nested)
  • ⚠️ Missing whitespace and escaped template tests (minor)

Security: 10/10

  • ✅ No credential exposure risk
  • ✅ No injection vulnerabilities
  • ✅ Input validation handled by existing resolve_value()

Performance: 10/10

  • ✅ No performance regression (just reordered logic)
  • ✅ Regex compiled once at module load (good)
  • ✅ Early return for simple templates (optimization)

📋 Recommendation

Approve with minor fixes

Before merge:

  1. Fix Warning docs: Set up Mintlify documentation (Task 93) #1 (stricter SIMPLE_TEMPLATE_PATTERN) - Recommended
  2. Add whitespace edge case test (Warning feat: unify LLM usage via Simon Willison's llm library (Task 95) #2) - Optional
  3. Update architecture docs (Warning feat: Smart output display for registry run #3) - Can be separate PR

After merge:


🏆 Highlights

  1. Excellent problem analysis: The implementation plan is thorough and shows deep understanding
  2. Comprehensive testing: 1620 insertions across 13 files, all tests pass
  3. Clear documentation: Progress log and implementation plan are exemplary
  4. Follows project standards: Uses make test, make check, proper typing, modern Python

Great work! This is exactly the kind of careful, well-tested change that builds a solid codebase.


Review completed by Claude Code - pflow code reviewer
Task-ID: 103

@spinje spinje merged commit c5434ee into main Jan 1, 2026
7 checks passed
spinje added a commit that referenced this pull request Jan 4, 2026
Add task specification for markdown-based workflow authoring format that
compiles to IR. Optimizes for LLM authoring with literate programming,
lintable code blocks, and token efficiency.

Also:
- Mark Task 103 as complete (merged in PR #32)
- Reorganize roadmap: v0.7.0 for LLM authoring, v0.8.0 for expressiveness
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant