name: score-signal

# Turn a CI scorecard REGRESSION into a specific, deduped GitHub issue the
# dispatch loop can resolve — the INTERNAL feeder of the fleet loop (the dual of
# the EXTERNAL idea-scout feeder). The scorecard ratchet (ci.yml) BLOCKS a
# regression at merge time; this job is the follow-through: on a daily cadence it
# folds the scorecard control pane, and for every metric whose debt rose above its
# pinned baseline (tools/scorecard_baseline.json) it files ONE scoped, lane-routed
# issue naming the owning RSI skill + the re-measure command + the `#N`-stamp
# contract. The dispatch loop then routes it to a lane and spawns a worker.
#
# AUTONOMOUS LIVE FILING (#980): the daily SCHEDULE now files the issues itself (it
# no longer only reports). This is the deliberate exception to the "scheduled jobs
# only report" convention — the point of this feeder is for CI to create the tracked
# ticket without a human. The storm bound is NOT a human in the loop but the feeder's
# own discipline: label-scoped dedup means a steady regression files exactly ONE open
# issue (then only REFRESHES it via #981 when it worsens — no daily re-file), and the
# worst-first --max-issues cap (default 5) bounds a multi-metric burst. The plan is
# still surfaced to the step summary + an artifact every run. A manual
# `workflow_dispatch` with live=true remains for an off-cadence arm; the pure
# plan/dedup logic is proven by tools/score_signal_test.py (run FIRST below), so a
# filed issue is a real signal, not a tool that always fires. Posture rationale +
# soak evidence: docs/notes/SCORE-SIGNAL-AUTO-ARM-2026-06-27.md.
on:
  schedule:
    # Daily 07:41 UTC — an off-the-hour minute clear of the other scheduled jobs
    # (garden 06:23, dogfood 06:41, dogfood-coverage 07:13, cadence Mon 08:17).
    - cron: "41 7 * * *"
  workflow_dispatch:
    inputs:
      live:
        description: "Actually CREATE the issues (default: dry-run plan only)"
        type: boolean
        default: false
      min_delta:
        description: "A metric must rise by at least this debt to file"
        type: string
        default: "1"
      max_issues:
        description: "Hard cap on issues filed per run (worst-regression-first)"
        type: string
        default: "5"
      worsen_delta:
        description: "An already-open ticket is REFRESHED only when its delta grew by at least this (#981)"
        type: string
        default: "2"

permissions:
  contents: read
  issues: write

concurrency:
  group: score-signal
  cancel-in-progress: false

jobs:
  score-signal:
    name: scorecard regression -> issue
    if: github.ref_name == 'main' || github.ref_name == 'master'
    runs-on: ubuntu-latest
    timeout-minutes: 30
    env:
      GOTOOLCHAIN: auto
      GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
    steps:
      - uses: actions/checkout@v4

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

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

      # The plan/dedup logic FIRST: a planned issue below is only meaningful if the
      # pure core that produced it is itself proven correct.
      - name: score-signal self-test
        run: python tools/score_signal_test.py

      # Fold the scorecard control pane. Tolerate a non-zero exit (the pane reds on
      # any residual debt) — score-signal reads the trend, not the exit code, and
      # acts only on a real measured rise.
      - name: fold scorecard control pane
        run: |
          set +e
          python tools/scorecard_control_pane.py --json > pane.json
          echo "pane-exit=$?"
          set -e
          test -s pane.json || { echo "control pane produced no JSON"; exit 1; }

      # Plan the issues (dry-run) — always, so the step summary shows what WOULD be
      # filed AND what would be refreshed even on an armed run. Operator inputs are
      # bound to env (not spliced into the script body) so an input value can't break
      # out into shell.
      - name: plan score-signal issues
        env:
          MIN_DELTA: ${{ github.event.inputs.min_delta || '1' }}
          MAX_ISSUES: ${{ github.event.inputs.max_issues || '5' }}
          WORSEN_DELTA: ${{ github.event.inputs.worsen_delta || '2' }}
        run: |
          python tools/score_signal.py --from pane.json --min-delta "$MIN_DELTA" \
            --max-issues "$MAX_ISSUES" --worsen-delta "$WORSEN_DELTA" --json > score-signal-plan.json
          python tools/score_signal.py --from pane.json --min-delta "$MIN_DELTA" \
            --max-issues "$MAX_ISSUES" --worsen-delta "$WORSEN_DELTA" > score-signal-plan.txt
          cat score-signal-plan.txt

      # File NEW issues + REFRESH worsened open ones. AUTONOMOUS (#980): the daily
      # SCHEDULE files live, and a manual `workflow_dispatch` with live=true still
      # arms an off-cadence run. The storm bound is the feeder's own discipline —
      # label-scoped dedup (a steady regression files once then only REFRESHES via
      # #981) + the worst-first --max-issues cap — so a daily live run cannot
      # duplicate or storm.
      - name: file score-signal issues (autonomous on schedule, or armed dispatch)
        if: ${{ github.event_name == 'schedule' || (github.event_name == 'workflow_dispatch' && github.event.inputs.live == 'true') }}
        env:
          MIN_DELTA: ${{ github.event.inputs.min_delta || '1' }}
          MAX_ISSUES: ${{ github.event.inputs.max_issues || '5' }}
          WORSEN_DELTA: ${{ github.event.inputs.worsen_delta || '2' }}
        run: |
          python tools/score_signal.py --from pane.json --min-delta "$MIN_DELTA" \
            --max-issues "$MAX_ISSUES" --worsen-delta "$WORSEN_DELTA" --live

      - name: payload -> step summary
        if: always()
        run: |
          {
            echo "## score-signal plan"
            armed="${{ (github.event_name == 'schedule' || (github.event_name == 'workflow_dispatch' && github.event.inputs.live == 'true')) && 'LIVE (issues filed)' || 'dry-run (nothing filed)' }}"
            echo "_Mode: ${armed}. The daily schedule files autonomously (#980); a manual workflow_dispatch needs live=true. Dedup + the worst-first cap bound a storm._"
            echo '```'
            cat score-signal-plan.txt 2>/dev/null || echo "(no plan)"
            echo '```'
            echo '<details><summary>machine plan</summary>'
            echo
            echo '```json'
            cat score-signal-plan.json 2>/dev/null || echo "{}"
            echo '```'
            echo '</details>'
          } >> "$GITHUB_STEP_SUMMARY"

      - name: upload plan artifact
        if: always()
        uses: actions/upload-artifact@v4
        with:
          name: score-signal-plan
          path: |
            score-signal-plan.json
            score-signal-plan.txt
            pane.json
          if-no-files-found: ignore
