name: cadence report

# The consolidated regular-cadence report — folds the three dimensions an
# operator tracks into ONE control-pane envelope on a weekly cadence:
#
#   * SCORES    — the scorecard control pane (portfolio debt + trend).
#   * WORK-DONE — git commits + `(fak ` ship-trailer count over a trailing window
#                 (the dimension that had NO durable cadence report before this).
#   * RELEASES  — the release-status fold (latest tag + next action).
#
# Today each lands in a SEPARATE place (garden.yml folds scores daily,
# release-cadence.yml folds releases every 6h, work-done was nowhere). This makes
# the fold default-on weekly, surfaced to the run's step summary and uploaded as a
# downloadable artifact, with a per-tick TREND vs the durable ledger
# (docs/cadence/history.jsonl).
#
# Fleet's cadence report is dry-run-first. Scheduled ticks extend the ledger
# locally but do not push. A manual dispatch with dry_run=false is the explicit
# arm for commit -> push, matching the release-cadence convention that scheduled
# jobs report (they don't auto-commit the shared trunk — only an explicit manual
# arm does, cf. release-cadence.yml). The pure fold/trend logic is proven by
# internal/cadencereport's unit tests (run first below), so a green report is a
# real signal, not a tool that always passes.
on:
  schedule:
    # Mondays 08:17 UTC — an off-the-hour minute clear of the other scheduled
    # jobs (garden 06:23, dogfood 06:41, dogfood-coverage 07:13, security-audit
    # Mon 06:17) so the weekly cadence tick doesn't pile onto them.
    - cron: "17 8 * * 1"
  workflow_dispatch:
    inputs:
      dry_run:
        description: "Extend the ledger locally, but do not commit or push"
        type: boolean
        default: true
      window_days:
        description: "Trailing window (days) the work-done dimension counts over"
        type: string
        default: "7"

permissions:
  contents: read

concurrency:
  group: cadence-report
  cancel-in-progress: false

jobs:
  cadence:
    name: consolidated cadence report
    if: github.ref_name == 'main' || github.ref_name == 'master'
    runs-on: ubuntu-latest
    timeout-minutes: 30
    permissions:
      contents: write
    env:
      GOTOOLCHAIN: auto
    steps:
      - uses: actions/checkout@v4
        with:
          # The work-done dimension counts commits over a trailing window, so the
          # full history must be present (a shallow checkout would undercount).
          fetch-depth: 0

      - uses: actions/setup-go@v5
        with:
          go-version-file: go.mod
          cache: false

      - uses: actions/setup-python@v5
        with:
          python-version: "3.x"

      # The fold/trend logic FIRST: a green report below is only meaningful if the
      # fold that produced it is itself proven correct. The milestone fold rides the
      # SAME loop cadence (issue #1443) so a milestone regression surfaces on the
      # operator's weekly read, not only the separate milestone-tick.yml cron.
      - name: cadence fold self-test
        run: go test ./internal/cadencereport/

      - name: milestone fold self-test
        run: go test ./internal/milestonereport/

      # The REPORT. Tolerate a non-zero exit (a flaky sub-tool must not suppress
      # the report) — the verdict is surfaced below, not swallowed.
      - name: cadence report
        run: |
          window="${{ github.event_name == 'workflow_dispatch' && inputs.window_days || '7' }}"
          set +e
          go run ./cmd/fak cadence --json --append-history --window "$window" > cadence-report.json
          echo "cadence-exit=$?"
          set -e
          python3 - <<'PY' > cadence-report.txt || echo "(report unreadable)" > cadence-report.txt
          import json
          d = json.load(open("cadence-report.json"))
          s, w, r = d["scores"], d["work"], d["releases"]
          print(f"{d['verdict']} ({d['finding']}) @{d['commit']} {d['date']}")
          print(f"  scores    debt {s['debt']} across {s.get('measured', 0)} scorecard(s); trend {s.get('trend_direction')}")
          print(f"  work      {w['commits']} commit(s) / {w['ships']} ship(s) in {w['window_days']}d")
          print(f"  releases  {r['version']}; next {r['action_kind']}")
          if d.get("trend"):
              print(f"  trend     {d['trend']['summary']}")
          print(f"  -> {d['next_action']}")
          PY
          cat cadence-report.txt

      # MILESTONES on the SAME loop cadence (issue #1443). The milestone report's
      # advisory --check gate runs as a sibling RSI surface here so a stalled roadmap
      # or an unmeasured climb is visible on the operator's weekly read — the same
      # cadence the scores/work/releases fold rides. Fail-open like the cadence report:
      # a non-zero exit (the advisory gate, or a partial roadmap) is surfaced, not
      # swallowed, so a flaky sub-tool never suppresses the report.
      - name: milestone report (--check)
        if: always()
        env:
          GH_TOKEN: ${{ github.token }}
        run: |
          set +e
          go run ./cmd/fak milestone report --json --check > milestone-report.json
          echo "milestone-check-exit=$?"
          set -e
          test -s milestone-report.json || echo '{}' > milestone-report.json
          python3 - <<'PY' > milestone-report.txt || echo "(milestone report unreadable)" > milestone-report.txt
          import json
          d = json.load(open("milestone-report.json"))
          print(f"{d.get('verdict','?')} ({d.get('finding','')}) @{d.get('commit','')} {d.get('date','')}")
          m = d.get("maturity") or {}
          if isinstance(m, dict) and m:
              print(f"  climb     {m.get('progress_pct',0):.1f}% progress; {m.get('matured',0)}/{m.get('cells',0)} matured; highest {m.get('highest','?')}")
          e = d.get("epics") or {}
          if isinstance(e, dict) and e:
              print(f"  roadmap   {e.get('overall_pct',0):.1f}% complete; {e.get('measured',0)}/{e.get('tracked',0)} epic(s) measured")
          if d.get("trend"):
              print(f"  trend     {d['trend'].get('summary','')}")
          if d.get("next_action"):
              print(f"  -> {d['next_action']}")
          PY
          cat milestone-report.txt

      - name: Dry-run stop
        if: >-
          github.event_name == 'schedule' ||
          (github.event_name == 'workflow_dispatch' && inputs.dry_run)
        run: |
          echo "## Dry-run only" >> "$GITHUB_STEP_SUMMARY"
          echo "Scheduled ticks and default manual runs stop after extending the ledger locally. Dispatch with dry_run=false to commit and push." >> "$GITHUB_STEP_SUMMARY"

      - name: Commit and push ledger
        if: >-
          github.event_name == 'workflow_dispatch' &&
          inputs.dry_run == false
        run: |
          set -euo pipefail
          git config user.name "fleet-cadence[bot]"
          git config user.email "fleet-cadence@users.noreply.github.com"
          git add docs/cadence/history.jsonl
          if git diff --staged --quiet; then
            echo "No ledger changes to commit"
          else
            git commit -s -m "docs(cadence): record cadence tick (fak docs)"
            git push
          fi

      - name: payload -> step summary
        if: always()
        run: |
          {
            echo "## cadence report"
            echo '```'
            cat cadence-report.txt 2>/dev/null || echo "(no report)"
            echo '```'
            echo '<details><summary>control-pane payload</summary>'
            echo
            echo '```json'
            cat cadence-report.json 2>/dev/null || echo "{}"
            echo '```'
            echo '</details>'
            echo
            echo "## milestones (same-cadence --check)"
            echo '```'
            cat milestone-report.txt 2>/dev/null || echo "(no milestone report)"
            echo '```'
            echo
            if [ "${{ github.event_name }}" = "schedule" ] || [ "${{ inputs.dry_run }}" = "true" ]; then
              echo "_Dry-run: the ledger was extended locally but not pushed. Dispatch with dry_run=false to commit and push._"
            else
              echo "_Ledger committed and pushed._"
            fi
          } >> "$GITHUB_STEP_SUMMARY"

      - name: upload report artifact
        if: always()
        uses: actions/upload-artifact@v4
        with:
          name: cadence-report
          path: |
            cadence-report.json
            cadence-report.txt
            milestone-report.json
            milestone-report.txt
          if-no-files-found: ignore
