Skip to content

♻️ Tiger Template - Move static dashboard chrome out of the model's output path (deterministic template-fill) #125

Description

@suiyangqiu

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type: RefactorA code quality improvement e.g. Tech debt

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions