Skip to content

Daily report renderer (ja/en) with golden tests #21

Description

@Saber5656

Title

Daily report renderer (ja/en) with golden tests

Summary

Implement the deterministic Markdown renderer for daily reports: embedded
text/template templates for Japanese and English, the section structure of
DESIGN.md §9.1, escaping via internal/sanitize, and byte-exact golden
tests.

Context

The deterministic report IS the product (the LLM only adds a narrative
paragraph). Golden tests here become the regression net for every upstream
change, so template output must be stable, timezone-explicit, and
locale-independent.

Scope

  • internal/render/render.go, funcs.go,
    templates/daily.ja.md.tmpl, templates/daily.en.md.tmpl (embedded via
    embed.FS), testdata/golden/*.md, tests.

Detailed Requirements

  1. func Daily(r *model.Report) (string, error) — selects template by
    r.Lang (ja/en only; anything else = error, config already
    validated).
  2. Section structure exactly per DESIGN §9.1, in order:
    1. # 日報 2026-07-10 (木) / # Daily report 2026-07-10 (Thu) — date +
      weekday; timezone shown when not system-local? Simpler fixed rule:
      always show IANA zone in the header line's suffix
      (— Asia/Tokyo).
    2. Summary line: totals sentence (commits, sessions, commands, active
      span HH:MM–HH:MM); zero-activity day renders the explicit
      no-activity sentence instead (ja: 記録された活動はありません。 /
      en: No recorded activity.) and skips sections 3–5.
    3. Narrative: rendered only when r.Narrative.Text != "", under heading
      ## サマリー(ローカルLLM: <model>) / ## Summary (local model: <model>). Allowed narrative surface: plain paragraphs only — the
      renderer applies sanitize.EscapeText per line (control chars were
      already stripped by 24; this pass additionally HTML-escapes </&
      and neutralizes leading list/heading markers), so LLM output cannot
      inject headings, HTML, or list structure into the report.
    4. Per-project sections ## <project> — the project name is rendered
      through sanitize.EscapeText (heading position is text context;
      neutralizes structure forgery; the name was already
      sanitized+redacted in aggregation): commits table
      (| time | hash | subject | +/− |, hash in backticks, subject via
      sanitize.EscapeCell), sessions table
      (| agent | span | msgs | tasks | title |, title via EscapeCell).
      Omit an empty table entirely (no headers over nothing).
    5. ## シェル操作 / ## Shell activity: | command | count | first–last |
      top groups + total line; omitted when zero commands.
    6. Warnings (when any): ## 注記 / ## Notes — bullet per warning
      (- [source] code: message, message via EscapeText).
    7. Footer (always): one line —
      Generated by worklog <version> · sources: zsh,git,… · LLM: <model|deterministic> · redaction: on|off.
  3. Formatting rules: times 15:04 in report tz; dates 2006-01-02;
    weekday names: ja (月火水木金土日), en Mon… — via lookup tables, NOT
    locale (time.Format weekday is English-only; map it).
    Insertions/deletions cell: +12/−3 (U+2212 not needed — ASCII hyphen).
    All numbers via strconv (no locale grouping).
  4. Template funcs registered from internal/sanitize only (escCell,
    escText) plus tiny format helpers; NO business logic in templates
    (tables built from prepared row structs in Go).
  5. Output ends with exactly one trailing newline; no trailing spaces on any
    line (golden-enforced; makes diffs clean).
  6. Golden tests: fixtures for (ja, en) × (full report incl. narrative +
    warnings, empty day, no-narrative day, adversarial content report —
    subjects with |, backticks, &/< (ANSI already stripped upstream),
    long Japanese titles, a malicious project name like
    # fake | heading, and a narrative containing ## injected +
    <script>). -update flag regenerates goldens.

Acceptance Criteria

  • Golden files byte-exact across macOS/Linux CI (no locale/tz leakage —
    tests set TZ=UTC explicitly and the Report carries its own zone).
  • Adversarial golden proves table integrity: a subject with | renders
    as one cell (the rendered file re-parsed with a naive | split gives
    the expected column count per row — asserted in code, not by eye).
  • Malicious project name renders as literal text in the heading (no new
    structure); adversarial narrative renders with ## injected and
    <script> neutralized (heading count of the document unchanged —
    asserted in code).
  • Empty-day golden contains the no-activity sentence and no empty
    tables/headings.
  • Weekday mapping correct for all 7 days (table test).
  • ja and en templates produce structurally identical documents
    (same heading count/order) for the same Report — structural test.
  • No template executes user content as template syntax
    ({{ in a subject stays literal — injection test).
  • ≥ 90% coverage on render package.

Validation

go test -race -cover ./internal/render/ in PR; attach one full ja golden
file in the PR description for human review of tone/format.

Dependencies

06, 18.

Non-goals

Standup templates (21), custom/user templates (v2), HTML output, file
writing (22).

Design References

  • docs/DESIGN.md §9.1, §12.2 (markdown injection row), §1.1 (report
    languages)

Source of truth: docs/issues/20-render-daily.md (PR #1, branch docs/v1-design). If this issue and the repo docs disagree, the docs win. Execution order and dependencies: docs/ISSUE_PLAN.md (this is issue 20 of 33).

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions