Bug
`substituteFields` in server/src/preFlight/fillTemplates.js only does in-string ${key} replacement when the field value is a string. Numeric and boolean values silently fall through the embedded-string path, so a YAML-native int like roundN: 1 leaves ${roundN} literal inside longer strings such as round_${roundN}_choice. The whole-value "${key}" regex (which JSON-stringifies) already works for all types — only the embedded-string path is missing.
The in-line comment already describes the intended behaviour as "string or number" — only the implementation forgot the number case:
// if the value is just a string or number, we can also replace instances of ${key} within other strings
if (typeof value === "string") { // ← should also accept number / boolean
const stringReplacementRegex = new RegExp(`\\$\\{${key}\\}`, "g");
stringifiedTemplate = stringifiedTemplate.replace(
stringReplacementRegex,
value,
);
}
Repro
substituteFields({
templateContent: { name: "round_${roundN}_choice" },
fields: { roundN: 1 },
});
// → { name: "round_${roundN}_choice" } ← bug, placeholder survives
With roundN: "1" (string), substitution works. With roundN: 1 (number), it doesn't.
This was originally surfaced in deliberation-lab/stagebook (which has its own copy of fillTemplates) when a Prisoner's Dilemma example used a ${roundN} broadcast over numeric round IDs and the viewer prompted for a roundN field at study start. We fixed it there in deliberation-lab/stagebook#276 — same fix should apply here.
Suggested fix
Extend the type check to cover all stringifiable scalars and use String(value) to handle non-string values cleanly:
if (
typeof value === "string" ||
typeof value === "number" ||
typeof value === "boolean"
) {
const stringReplacementRegex = new RegExp(`\\$\\{${key}\\}`, "g");
stringifiedTemplate = stringifiedTemplate.replace(
stringReplacementRegex,
String(value),
);
}
The whole-value "${key}" replacement above this block already handles object/array values via JSON.stringify, so this branch only needs to cover scalars that can meaningfully be embedded in a string. Booleans get the same treatment for symmetry — ${flag} → "true" / "false".
Suggested regression tests (mirroring what we added in stagebook):
- Numeric field embedded in a string:
{ count: "${num}" } + { num: 42 } → { count: 42 } (already works), but { name: "x_${num}_y" } + { num: 42 } → { name: "x_42_y" } (the regression).
- Numeric broadcast field substituted into stage names — exact repro shape.
- Boolean field embedded in another string for symmetry.
Bug
`substituteFields` in
server/src/preFlight/fillTemplates.jsonly does in-string${key}replacement when the field value is a string. Numeric and boolean values silently fall through the embedded-string path, so a YAML-native int likeroundN: 1leaves${roundN}literal inside longer strings such asround_${roundN}_choice. The whole-value"${key}"regex (which JSON-stringifies) already works for all types — only the embedded-string path is missing.The in-line comment already describes the intended behaviour as "string or number" — only the implementation forgot the number case:
Repro
With
roundN: "1"(string), substitution works. WithroundN: 1(number), it doesn't.This was originally surfaced in deliberation-lab/stagebook (which has its own copy of
fillTemplates) when a Prisoner's Dilemma example used a${roundN}broadcast over numeric round IDs and the viewer prompted for aroundNfield at study start. We fixed it there in deliberation-lab/stagebook#276 — same fix should apply here.Suggested fix
Extend the type check to cover all stringifiable scalars and use
String(value)to handle non-string values cleanly:The whole-value
"${key}"replacement above this block already handles object/array values viaJSON.stringify, so this branch only needs to cover scalars that can meaningfully be embedded in a string. Booleans get the same treatment for symmetry —${flag}→"true"/"false".Suggested regression tests (mirroring what we added in stagebook):
{ count: "${num}" }+{ num: 42 }→{ count: 42 }(already works), but{ name: "x_${num}_y" }+{ num: 42 }→{ name: "x_42_y" }(the regression).