# L1 Funnel And Non-L1 Exclusion

## Current Design

- Jira snapshots, initial jobs, worker gate decisions and runs are canonical in
  MySQL. Feishu Base is a ticket-level downstream projection.
- The Base projection joins only the latest Auto Coverage details. It does not
  expose the current L1 Gate decision, gate-time oncaller or a cumulative
  funnel.
- `In Auto-oncall Scope` currently means that an initial job exists, is not an
  unsupported OP case and its latest status is not `skipped`. This collapses
  gate admission and execution outcome into one value.
- The non-automatic-reason chart intentionally includes business-gate skips.
  A later non-L1 reassignment is excluded only after the initial job is
  reclassified to `oncaller_not_confirmed_l1`.
- Post-answer oncaller reconciliation currently scans only `succeeded` jobs
  with a mapped Jira internal comment. Coverage-skipped jobs such as ST-2815
  are never reconsidered, so their latest assignee can be non-L1 while their
  old coverage category still appears in the fallout chart.

## Risks

- Rewriting historical run or original classification rows would destroy the
  audit of what happened at execution time.
- Keeping non-L1 tickets in coverage fallout would make the optimizer and
  dashboard attribute population-boundary changes to answerability problems.
- Reclassifying a succeeded job before invalidating its mapped Jira internal
  comment could leave a visible stale answer with no retry candidate.
- Reclassifying an active queued/running/retryable job could race the worker.
- Updating Base fields or charts before the code contract is tested could make
  the visible funnel disagree with MySQL.

## Target Contract

```text
all observed tickets
  -> current L1 Gate allowed: L1 business denominator
       -> current Auto Coverage Gate allowed: coverage admitted / analysis started
            -> latest job and run succeeded: analysis succeeded
                 -> decision = internal_only: automatically handled

current L1 Gate skipped(oncaller_not_confirmed_l1)
  -> non-L1 exclusion lane only
  -> never enters coverage fallout, execution status, success or handling metrics
```

- MySQL remains canonical. Base never writes business state back to MySQL.
- The original gate and run timeline remains auditable, while the latest L1
  Gate decision is the current population boundary used by Base.
- `L1 Eligible Value = 1` only when the latest persisted initial
  `l1_oncaller_gate` decision is `allowed`.
- `Coverage Admitted Value = 1` only when current L1 eligibility is `1` and the
  latest initial `auto_coverage_gate` decision is `allowed`.
- `Analysis Succeeded Value = 1` only when coverage is admitted, the latest
  initial job is `succeeded` and its latest run is `succeeded`.
- `Automatically Handled Value = 1` only when analysis succeeded and the latest
  run decision is `internal_only`.
- `In Auto-oncall Scope` remains as a compatibility field but becomes exactly
  equivalent to `Coverage Admitted Value`.
- A confirmed non-L1 ticket has `L1 Eligible Value = 0`,
  `Non-L1 Excluded Value = 1`, `Funnel Exclusion Reason = non_l1`, and no
  Auto-oncall category. It may be counted only in a separate exclusion
  card/chart.
- Coverage fallout categories are populated only for current L1-eligible
  tickets whose coverage decision is not allowed.
- Gate-time oncaller and latest Jira oncaller are separate Base fields.

### Reconciliation Contract

- Recheck terminal initial jobs (`succeeded`, `skipped`,
  `permanently_failed`) whose current L1 Gate is still `allowed` and whose Jira
  creation time is inside the configured rolling window.
- Do not reclassify queued, running or retryable jobs.
- For `succeeded` jobs, require the mapped internal comment and update it first;
  only then update canonical DB state.
- For skipped or permanently failed jobs, no automatic Jira answer exists to
  invalidate; update DB state directly after confirmed non-L1 evidence.
- The DB transaction marks the latest job `skipped/oncaller_not_confirmed_l1`,
  replaces the current L1 Gate decision, removes current L1 population and
  leaves historical runs and Auto Coverage gate evidence unchanged.
- Immediately refresh the ticket's Base projection after DB correction.

### Base Funnel

- Add compact explanatory text describing the denominator and the non-L1 exit
  lane.
- Add metric cards for total observed, current L1 eligible, coverage admitted,
  analysis succeeded and automatically handled, plus a separate non-L1
  exclusion card.
- Add an `Auto-oncall Funnel Summary` helper table with one row per ordered
  stage (`Stage Key`, `Stage Label`, `Stage Order`, `Stage Value`, `Lane`,
  `Definition`, `Synced At`). It is a downstream Base projection only; MySQL
  ticket analytics remains canonical.
- Populate the helper table from full-population aggregates over canonical
  ticket facts, independently of the bounded recent batch used to incrementally
  refresh Ticket rows. Do not re-fetch Jira or rewrite all historical Base rows
  to refresh the aggregate. Render the main-lane rows with Base's dedicated
  funnel block. The non-L1 row is filtered into its own card and never mixed
  into the cumulative main funnel.
- Keep non-L1 exclusions outside the `不可自动处理原因` chart.

## Implementation Steps

1. Add current L1 decision/details and Auto Coverage decision to the ticket
   analytics query and row contract.
2. Project explicit cumulative funnel values, gate-time oncaller, exclusion
   reason and compatible `In Auto-oncall Scope` semantics to Base.
3. Expand post-answer reconciliation to terminal coverage-skipped/failed jobs,
   while preserving Jira-first ordering for succeeded jobs.
4. Add contract tests for the funnel, non-L1 category suppression, target SQL,
   repeat safety, comment ordering and Base refresh behavior.
5. Add the helper-table projection and configuration, create the Base fields,
   update/add dashboard blocks and run a bounded historical reconciliation and
   backfill from canonical Jira/MySQL state.

## Verification Checklist

- Local projection tests cover no job, L1 denied, coverage denied, admitted
  failed, succeeded human-required and automatically handled tickets.
- ST-2815-shaped coverage-skipped reassignment becomes non-L1 without a Jira
  comment update and disappears from coverage fallout.
- A succeeded reassignment still updates the exact mapped Jira comment before
  DB state.
- Active jobs are not selected by reconciliation.
- Repeat reconciliation is idempotent.
- `npm run typecheck`, focused tests, full `npm test` and `git diff --check`
  pass before any rollout.
- Base field readback matches the target schema.
- Dashboard block data reconciles to canonical counts for the same Base ticket
  set, and ST-2815 has `non_l1` exclusion with no Auto-oncall category.
- If deployed, verify the live pod image/env, reconciliation logs, DB job/gate
  state and Base record values; do not treat a source merge as rollout proof.

## Non-Goals

- No poller gate-order change.
- No continuation behavior change.
- No customer-visible reply, issue transition or production operation.
- No mutation of historical run rows or Auto Coverage gate evidence.
- No Base-to-DB synchronization or report-derived fallback.
- No prod automation enablement or prod Base synchronization.
