Skip to content

feat: add schedule heatmap calendar to --stats output#29587

Merged
pelikhan merged 3 commits intomainfrom
copilot/update-stats-argument-calendar-view
May 1, 2026
Merged

feat: add schedule heatmap calendar to --stats output#29587
pelikhan merged 3 commits intomainfrom
copilot/update-stats-argument-calendar-view

Conversation

Copy link
Copy Markdown
Contributor

Copilot AI commented May 1, 2026

--stats currently shows a file-size table but gives no visibility into when workflows run. This adds a 7×24 UTC heatmap (days × hours) so users can immediately spot scheduling hotspots.

Changes

  • compile_schedule_calendar.go — new file with:
    • Cron field parser supporting *, ranges, steps, and comma lists
    • buildScheduleGrid — aggregates on.schedule[*].cron from all workflows into a [7][24]int trigger-count grid
    • displayScheduleCalendar — renders the heatmap to stderr using block characters (·░▒▓█), colour-coded in TTY mode; no-ops when no scheduled workflows exist
  • WorkflowStats — adds Schedules []string, populated by extracting on.schedule[*].cron from the parsed lock YAML
  • outputResults — calls displayScheduleCalendar after displayStatsTable; skipped in JSON output mode
  • compileAllFilesInDirectory — when --stats is set and no explicit files were given, forwards the discovered file list into config.MarkdownFiles before outputResults, so the heatmap works in the "compile everything" path (previously stats were silently empty in this case)

Example output

ℹ Schedule Heatmap (UTC)

      00 01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 21 22 23
Mon   ·  ·  ·  ·  ·  ·  ·  ·  ░  ▒  ░  ·  ·  ·  ▓  ·  ·  ·  ·  ·  ░  ·  ·  ·
Tue   ·  ·  ·  ·  ·  ·  ·  ·  ░  ▒  ░  ·  ·  ·  ▓  ·  ·  ·  ·  ·  ░  ·  ·  ·
...
Legend: · = 0   ░ = 1   ▒ = 2-3   ▓ = 4-6   █ = 7+

Copilot AI and others added 2 commits May 1, 2026 17:12
When --stats is enabled (and JSON output is not), display a 7×24
calendar-style heatmap showing how many workflows fire at each
hour/day-of-week (UTC). Intensity is shown using block characters
(·░▒▓█) and colour-coded in terminal mode.

- WorkflowStats gains a Schedules []string field, populated from
  on.schedule[*].cron in the compiled lock YAML
- compile_schedule_calendar.go: cron field parser, schedule grid
  builder, and displayScheduleCalendar renderer
- outputResults: calls displayScheduleCalendar after displayStatsTable
- compileAllFilesInDirectory: forwards discovered mdFiles into
  config.MarkdownFiles before outputResults so --stats works when no
  explicit files are given
- 19 unit tests in compile_schedule_calendar_test.go

Agent-Logs-Url: https://github.com/github/gh-aw/sessions/8289092c-4b49-4824-bdfd-9f128955c5a2

Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
@pelikhan pelikhan marked this pull request as ready for review May 1, 2026 18:16
Copilot AI review requested due to automatic review settings May 1, 2026 18:16
@pelikhan pelikhan merged commit 965c512 into main May 1, 2026
18 checks passed
@pelikhan pelikhan deleted the copilot/update-stats-argument-calendar-view branch May 1, 2026 18:17
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds a 7×24 (UTC) schedule heatmap to --stats output so users can quickly see when workflows are scheduled to run and identify hourly/day-of-week hotspots.

Changes:

  • Extend WorkflowStats to collect on.schedule[*].cron expressions from lock YAML.
  • Add cron parsing + heatmap aggregation/rendering for schedule visualization in --stats.
  • Ensure the “compile all workflows” path populates MarkdownFiles so stats/heatmap works consistently.
Show a summary per file
File Description
pkg/cli/compile_stats.go Extracts cron strings from on.schedule[*].cron into WorkflowStats.Schedules.
pkg/cli/compile_schedule_calendar.go Implements cron field parsing, grid aggregation, and rendering of the schedule heatmap to stderr.
pkg/cli/compile_schedule_calendar_test.go Adds unit/integration-style tests for cron parsing and calendar rendering.
pkg/cli/compile_pipeline.go Wires the heatmap display into --stats output and ensures stats have access to file lists in the directory compile path.
docs/src/content/docs/agent-factory-status.mdx Updates a documented workflow schedule time.

Copilot's findings

Tip

Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

  • Files reviewed: 5/5 changed files
  • Comments generated: 2

Comment on lines +113 to +125
// normalised to 0).
func parseCronSchedule(cron string) (hours []int, daysOfWeek []int, err error) {
fields := strings.Fields(cron)
if len(fields) != 5 {
return nil, nil, fmt.Errorf("cron expression must have 5 fields, got %d: %q", len(fields), cron)
}

// Field index 1 is the hour field (0-23).
hours, err = parseCronField(fields[1], 0, 23)
if err != nil {
return nil, nil, fmt.Errorf("invalid hour field %q in cron %q: %w", fields[1], cron, err)
}

Copy link

Copilot AI May 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

parseCronSchedule currently only considers the hour (field 2) and day-of-week (field 5). This makes the heatmap inaccurate for real cron expressions in this repo that use non-wildcard minutes (e.g. "*/30 * * * *" should contribute 2 triggers/hour, not 1) and/or a non-wildcard day-of-month/month (e.g. "15 9 1 * " is monthly, but will be counted as daily). Consider parsing the minute field and adding its multiplicity into the per-hour bucket, and skipping (or explicitly warning + skipping) schedules where day-of-month or month aren’t "" since they can’t be represented correctly in a 7×24 grid.

Suggested change
// normalised to 0).
func parseCronSchedule(cron string) (hours []int, daysOfWeek []int, err error) {
fields := strings.Fields(cron)
if len(fields) != 5 {
return nil, nil, fmt.Errorf("cron expression must have 5 fields, got %d: %q", len(fields), cron)
}
// Field index 1 is the hour field (0-23).
hours, err = parseCronField(fields[1], 0, 23)
if err != nil {
return nil, nil, fmt.Errorf("invalid hour field %q in cron %q: %w", fields[1], cron, err)
}
// normalised to 0). Minute multiplicity is preserved by repeating each hour
// once per matching minute. Schedules with non-wildcard day-of-month or month
// fields are rejected because they cannot be represented accurately in a 7×24
// weekly heatmap.
func parseCronSchedule(cron string) (hours []int, daysOfWeek []int, err error) {
fields := strings.Fields(cron)
if len(fields) != 5 {
return nil, nil, fmt.Errorf("cron expression must have 5 fields, got %d: %q", len(fields), cron)
}
// The heatmap represents a weekly 7×24 grid, so only schedules that recur
// independent of day-of-month and month can be shown correctly.
if fields[2] != "*" {
return nil, nil, fmt.Errorf("day-of-month field %q in cron %q is not supported in weekly schedule heatmap", fields[2], cron)
}
if fields[3] != "*" {
return nil, nil, fmt.Errorf("month field %q in cron %q is not supported in weekly schedule heatmap", fields[3], cron)
}
// Field index 0 is the minute field (0-59). Its cardinality determines how
// many times each matching hour should contribute to the heatmap.
minutes, err := parseCronField(fields[0], 0, 59)
if err != nil {
return nil, nil, fmt.Errorf("invalid minute field %q in cron %q: %w", fields[0], cron, err)
}
// Field index 1 is the hour field (0-23).
parsedHours, err := parseCronField(fields[1], 0, 23)
if err != nil {
return nil, nil, fmt.Errorf("invalid hour field %q in cron %q: %w", fields[1], cron, err)
}
// Preserve minute multiplicity without changing the function signature by
// repeating each hour once per matching minute.
hours = make([]int, 0, len(parsedHours)*len(minutes))
for _, h := range parsedHours {
for range minutes {
hours = append(hours, h)
}
}

Copilot uses AI. Check for mistakes.
Comment on lines +88 to +92
func TestParseCronSchedule_Weekdays(t *testing.T) {
hours, days, err := parseCronSchedule("33 14 * * 1-5")
require.NoError(t, err, "weekday cron should parse without error")
assert.Equal(t, []int{14}, hours, "hour should be 14")
assert.Equal(t, []int{1, 2, 3, 4, 5}, days, "day-of-week 1-5 = Mon through Fri")
Copy link

Copilot AI May 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

parseCronSchedule tests here only exercise hour + day-of-week parsing. Since the heatmap shows trigger counts, please add cases for minute multiplicity (e.g. "*/30 * * * *" should add 2 triggers/hour) and for non-wildcard day-of-month/month (e.g. "15 9 1 * *"), so behavior stays correct once those fields are handled/skipped.

Copilot uses AI. Check for mistakes.
@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented May 1, 2026

🧪 Test Quality Sentinel Report

Test Quality Score: 78/100

⚠️ Acceptable — minor suggestions below

Metric Value
New/modified tests analyzed 26
✅ Design tests (behavioral contracts) 26 (100%)
⚠️ Implementation tests (low value) 0 (0%)
Tests with error/edge cases 11 (42%)
Duplicate test clusters 1
Test inflation detected No (295 test lines / 299 prod lines ≈ 1:1)
🚨 Coding-guideline violations None

Test Classification Details

View all 26 test classifications
Test File Classification Notes
TestParseCronField_Wildcard compile_schedule_calendar_test.go:19 ✅ Design Verifies expansion to all 24 hours
TestParseCronField_SingleValue compile_schedule_calendar_test.go:26 ✅ Design Verifies exact single-value output
TestParseCronField_Range compile_schedule_calendar_test.go:32 ✅ Design Verifies range expansion
TestParseCronField_Step compile_schedule_calendar_test.go:38 ✅ Design Verifies step expansion
TestParseCronField_RangeWithStep compile_schedule_calendar_test.go:44 ✅ Design Verifies combined range+step
TestParseCronField_CommaSeparated compile_schedule_calendar_test.go:50 ✅ Design Verifies list parsing
TestParseCronField_CommaSeparatedDeduplication compile_schedule_calendar_test.go:56 ✅ Design Edge case: deduplication
TestParseCronField_OutOfRange compile_schedule_calendar_test.go:62 ✅ Design Error case: out-of-range value
TestParseCronField_InvalidValue compile_schedule_calendar_test.go:67 ✅ Design Error case: non-numeric input
TestParseCronField_InvalidStep compile_schedule_calendar_test.go:72 ✅ Design Error case: zero step
TestParseCronSchedule_Daily compile_schedule_calendar_test.go:80 ✅ Design Verifies day/hour extraction
TestParseCronSchedule_Weekdays compile_schedule_calendar_test.go:87 ✅ Design Verifies weekday range
TestParseCronSchedule_MultipleHours compile_schedule_calendar_test.go:94 ✅ Design Verifies multiple hours
TestParseCronSchedule_Sunday7NormalisedTo0 compile_schedule_calendar_test.go:101 ✅ Design Edge case: Sunday alias normalization
TestParseCronSchedule_SundayDeduplicated compile_schedule_calendar_test.go:107 ✅ Design Edge case: 0-7 range dedup
TestParseCronSchedule_WrongFieldCount compile_schedule_calendar_test.go:113 ✅ Design Error case: wrong field count
TestBuildScheduleGrid_Empty compile_schedule_calendar_test.go:121 ✅ Design Edge case: empty input
TestBuildScheduleGrid_NoSchedules compile_schedule_calendar_test.go:127 ✅ Design Edge case: no schedule entries
TestBuildScheduleGrid_SingleDailyCron compile_schedule_calendar_test.go:134 ✅ Design Verifies grid counts
TestBuildScheduleGrid_MultipleWorkflows compile_schedule_calendar_test.go:146 ✅ Design Verifies count accumulation
TestBuildScheduleGrid_WeekdayOnly compile_schedule_calendar_test.go:158 ✅ Design Verifies weekday-only scheduling
TestIntensityChar compile_schedule_calendar_test.go:176 ✅ Design Table-driven; covers all thresholds including boundaries (0, 100)
TestDisplayScheduleCalendar_NoSchedules compile_schedule_calendar_test.go:195 ✅ Design Edge case: verifies no output
TestDisplayScheduleCalendar_WithSchedules compile_schedule_calendar_test.go:209 ✅ Design Verifies complete rendered output
TestDisplayScheduleCalendar_ContainsAllDayLabels compile_schedule_calendar_test.go:232 ✅ Design ⚠️ Partially duplicates day-label assertions in _WithSchedules
TestDisplayScheduleCalendar_ContainsAllHourHeaders compile_schedule_calendar_test.go:254 ✅ Design Verifies hour headers in output

Flagged Tests — Requires Review

⚠️ TestDisplayScheduleCalendar_ContainsAllDayLabels (compile_schedule_calendar_test.go:232)

Classification: Design test (low duplication value)
Issue: This test asserts that all calendarDayNames appear in output — the exact same property already verified by TestDisplayScheduleCalendar_WithSchedules. Deleting it would not expose any behavioral regression not already caught by the other test.
What design invariant does this test enforce? That day labels appear in rendered output — already covered.
What would break if deleted? Nothing that _WithSchedules wouldn't catch.
Suggested improvement: Remove this test and extend _WithSchedules with a comment noting it covers day labels, or consolidate all displayScheduleCalendar output checks into a single table-driven test to avoid the boilerplate duplication.


Language Support

Tests analyzed:

  • 🐹 Go (*_test.go): 26 tests — unit (//go:build !integration)
  • 🟨 JavaScript (*.test.cjs, *.test.js): 0 tests changed

Verdict

Check passed. 0% of new tests are implementation tests (threshold: 30%). All 26 tests verify observable outputs and behavioral contracts of the new schedule heatmap feature. No coding-guideline violations detected.

The test suite is strong overall: parseCronField has complete error coverage (out-of-range, non-numeric, zero step), parseCronSchedule tests Sunday alias normalization and deduplication edge cases, buildScheduleGrid covers both empty/no-schedule paths, and TestIntensityChar uses a table-driven approach covering all density thresholds. The one minor concern is the slight duplication among displayScheduleCalendar tests — consolidating them into a single table-driven test would improve clarity.


📖 Understanding Test Classifications

Design Tests (High Value) verify what the system does:

  • Assert on observable outputs, return values, or state changes
  • Cover error paths and boundary conditions
  • Would catch a behavioral regression if deleted
  • Remain valid even after internal refactoring

Implementation Tests (Low Value) verify how the system does it:

  • Assert on internal function calls (mocking internals)
  • Only test the happy path with typical inputs
  • Break during legitimate refactoring even when behavior is correct
  • Give false assurance: they pass even when the system is wrong

Goal: Shift toward tests that describe the system's behavioral contract — the promises it makes to its users and collaborators.

References: §25226761187

🧪 Test quality analysis by Test Quality Sentinel · ● 604.7K ·

Copy link
Copy Markdown
Contributor

@github-actions github-actions Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ Test Quality Sentinel: 78/100. Test quality is acceptable — 0% of new tests are implementation tests (threshold: 30%). All 26 tests verify behavioral contracts of the new schedule heatmap feature. No coding-guideline violations detected.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants