Problem
The dashboard generator asks the model to author the entire index.html (see processor/claudeRunner.js:140-151, "Generate the dashboard HTML to ..."). That output path includes static template chrome the model has no reason to author: the tailwind.config block (templates/dashboard.html:16), the sswColors constant (:493), Chart.defaults (:504), and the profile-image fallback handler (:523). None of these are {{PLACEHOLDER}} regions.
A single model typo in that static chrome can ship a broken dashboard. The tailwind.config case is the worst: an unquoted sans-serif parses as the expression sans - serif, throws ReferenceError at runtime, the whole tailwind.config assignment aborts, and every ssw-* colour utility silently produces no CSS. This is the bug behind #98, which recurred after PR #99's new Function() parse-check (it cannot catch runtime-only errors), and which PR #121 patched downstream with an unconditional overwrite.
Each of those fixes is a per-block detector or stamp. The next corrupted static block (chart setup, profile-image fallback) would need its own. That is a detector treadmill.
Root cause
The substitution of dynamic content into the template currently happens inside the model's generation rather than in deterministic code. The template already draws a clean line between "model owns this" (29 {{PLACEHOLDER}} regions) and "template owns this" (everything else). The bug class exists only because static chrome rides along in the model's output instead of coming straight from templates/dashboard.html.
Proposed fix
Move the fill from the model to deterministic code. The model emits only the values for the 29 placeholders; processor/index.js reads templates/dashboard.html and performs the template.replace("{{X}}", value) substitution itself. Static chrome then comes verbatim from the template and never enters the model's output path.
Preferred transport: fragment files, not a single JSON blob. The model writes each placeholder's content to its own raw-HTML file (e.g. projects/<project>/<meeting>/dashboard-parts/SUMMARY.html, PARTICIPANT_CARDS.html, ...), and index.js reads each file and substitutes. This avoids the JSON-escaping failure mode that large embedded-HTML JSON payloads are prone to (quotes, newlines, backslashes), which would otherwise trade a rare static-config typo for frequent parse failures.
Why this is better than the per-block guards
Risks / things to validate
- The model loses whole-file authorship. CLAUDE.md currently relies on model judgment for conditional behaviour (e.g. "if a ceremony was skipped, just omit it"). Confirm that emitting empty/omitted fragments for those cases produces the same result as today.
- The 29-placeholder contract must be exhaustive. Any region the model needs to vary that is not currently a placeholder has to become one, or this approach will be too rigid.
{{CHART_SCRIPTS}} is itself a placeholder (model-authored JS). Decide whether generated chart code stays model-authored (and thus still needs a parse check) or moves to a data-only contract where the template owns the chart-rendering code and the model supplies only datasets.
- Migration touches
claudeRunner.js (prompt rewrite), index.js (fill logic), and CLAUDE.md (instructions). Needs an end-to-end run on a real transcript before rollout.
Acceptance criteria
- The model's output for a dashboard contains no static chrome;
tailwind.config, sswColors, Chart.defaults, and the profile-image fallback come solely from templates/dashboard.html.
- Deterministic template-fill produces a byte-identical chrome region across runs for the same template.
- A corrupted/missing placeholder value degrades gracefully (empty section) rather than breaking the page.
processor/dashboardValidator.js's tailwind-stamp path is removed or reduced to a no-op once chrome is no longer model-authored.
Context
Supersedes the tactical fixes in #98 / PR #99 / PR #121. PR #121 is being closed in favour of this structural fix, since the corruption it guards against is rare and the per-block stamp does not generalise.
Problem
The dashboard generator asks the model to author the entire
index.html(seeprocessor/claudeRunner.js:140-151, "Generate the dashboard HTML to ..."). That output path includes static template chrome the model has no reason to author: thetailwind.configblock (templates/dashboard.html:16), thesswColorsconstant (:493),Chart.defaults(:504), and the profile-image fallback handler (:523). None of these are{{PLACEHOLDER}}regions.A single model typo in that static chrome can ship a broken dashboard. The
tailwind.configcase is the worst: an unquotedsans-serifparses as the expressionsans - serif, throwsReferenceErrorat runtime, the wholetailwind.configassignment aborts, and everyssw-*colour utility silently produces no CSS. This is the bug behind #98, which recurred after PR #99'snew Function()parse-check (it cannot catch runtime-only errors), and which PR #121 patched downstream with an unconditional overwrite.Each of those fixes is a per-block detector or stamp. The next corrupted static block (chart setup, profile-image fallback) would need its own. That is a detector treadmill.
Root cause
The substitution of dynamic content into the template currently happens inside the model's generation rather than in deterministic code. The template already draws a clean line between "model owns this" (29
{{PLACEHOLDER}}regions) and "template owns this" (everything else). The bug class exists only because static chrome rides along in the model's output instead of coming straight fromtemplates/dashboard.html.Proposed fix
Move the fill from the model to deterministic code. The model emits only the values for the 29 placeholders;
processor/index.jsreadstemplates/dashboard.htmland performs thetemplate.replace("{{X}}", value)substitution itself. Static chrome then comes verbatim from the template and never enters the model's output path.Preferred transport: fragment files, not a single JSON blob. The model writes each placeholder's content to its own raw-HTML file (e.g.
projects/<project>/<meeting>/dashboard-parts/SUMMARY.html,PARTICIPANT_CARDS.html, ...), andindex.jsreads each file and substitutes. This avoids the JSON-escaping failure mode that large embedded-HTML JSON payloads are prone to (quotes, newlines, backslashes), which would otherwise trade a rare static-config typo for frequent parse failures.Why this is better than the per-block guards
tailwind.config.processor/dashboardValidator.js(the unconditional-stamp logic from PR Always overwrite tailwind.config block instead of detect-and-repair #121 and thefindSyntaxErrormachinery from PR Repair corrupted tailwind.config in generated dashboards #99 become unnecessary for chrome).Risks / things to validate
{{CHART_SCRIPTS}}is itself a placeholder (model-authored JS). Decide whether generated chart code stays model-authored (and thus still needs a parse check) or moves to a data-only contract where the template owns the chart-rendering code and the model supplies only datasets.claudeRunner.js(prompt rewrite),index.js(fill logic), and CLAUDE.md (instructions). Needs an end-to-end run on a real transcript before rollout.Acceptance criteria
tailwind.config,sswColors,Chart.defaults, and the profile-image fallback come solely fromtemplates/dashboard.html.processor/dashboardValidator.js's tailwind-stamp path is removed or reduced to a no-op once chrome is no longer model-authored.Context
Supersedes the tactical fixes in #98 / PR #99 / PR #121. PR #121 is being closed in favour of this structural fix, since the corruption it guards against is rare and the per-block stamp does not generalise.