fix(prompts): make template token substitution order-independent
RenderTemplate replaced tokens one key at a time over a StringBuilder, so a token appearing inside an already-substituted value got substituted again on a later pass. The prompt briefs only escaped this because their callers happen to pass "tasks" last -- reordering the dictionary or adding a fourth token would have started rewriting user-authored task descriptions, which after the enhance phase carry file paths and config snippets. Single-pass regex over the template instead; unknown tokens still pass through.
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace ClaudeDo.Data;
|
||||
|
||||
@@ -193,14 +194,16 @@ public static class PromptFiles
|
||||
public static string Render(PromptKind kind, IReadOnlyDictionary<string, string> values)
|
||||
=> RenderTemplate(ReadOrDefault(kind), values);
|
||||
|
||||
/// <summary>Replace only the given {name} tokens; any other braces pass through untouched.</summary>
|
||||
private static readonly Regex TokenPattern = new(@"\{(\w+)\}", RegexOptions.Compiled);
|
||||
|
||||
/// <summary>Replace only the given {name} tokens; any other braces pass through untouched.
|
||||
/// Single pass over the template, so a token appearing inside a substituted VALUE is never
|
||||
/// re-substituted. That matters because the values are user-authored task titles and
|
||||
/// descriptions: a sharpened description mentioning a literal {repo} must survive verbatim,
|
||||
/// and it must not depend on the caller happening to order its dictionary correctly.</summary>
|
||||
public static string RenderTemplate(string template, IReadOnlyDictionary<string, string> values)
|
||||
{
|
||||
var sb = new StringBuilder(template);
|
||||
foreach (var (key, val) in values)
|
||||
sb.Replace("{" + key + "}", val);
|
||||
return sb.ToString();
|
||||
}
|
||||
=> TokenPattern.Replace(template, m =>
|
||||
values.TryGetValue(m.Groups[1].Value, out var val) ? val : m.Value);
|
||||
|
||||
public static string DefaultFor(PromptKind kind) => kind switch
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user