Data Model: CI Aggregate Source-Eligibility

No persistent storage. These are the in-process value objects of the pure decision resolve_source(...). All are plain, immutable data (dataclasses or tagged dicts — implementer's call; the contract is the shape + semantics).

Inputs

TriggerMeta — the triggering workflow_run's provenance

FieldTypeSourceNotes
eventstrgithub.event.workflow_run.eventpush \
head_branchstrgithub.event.workflow_run.head_branchthe source run's branch (PR branch for PR-head; main for push-main)
conclusionstrgithub.event.workflow_run.conclusionsuccess \

CandidateRun — one entry from gh run list --json databaseId,conclusion,headBranch,event,...

FieldTypeNotes
databaseIdintresolved into run-id when eligible
conclusionstronly success is an eligible backfill source (FR-005 / LEAK-A)
headBranchstrmust be in {source_branch, default_branch} (FR-005 / LEAK-B)
eventstr(carried for future assertions; not required for v1 decision)

candidates are already scoped by the workflow to the ci-modules.yml workflow; the pure function applies the success + branch filters (it does not trust the query alone).

Output — SourceDecision (a tagged union; exactly one variant)

VariantCarriesrun-id outputeligibility slugWhen
EligibleSourcerun_id: int<run_id>eligible-sourceeligible provenance + a success run on {source ∪ default}
NoEligibleSourcereason: str""no-success-sourceeligible provenance but no success candidate
NotAMainSourcereason: str""pr-head-trigger \failed-trigger
NoFallback—"" (step skipped upstream)dispatch-no-fallbackevent=workflow_dispatch

Invariants (testable — map to reviewer INV-*)

  • INV-A (success-only): no EligibleSource ever carries a conclusion != success run. (FR-005)
  • INV-B (branch-scope): no EligibleSource ever carries a headBranch ∉ {source, default} run. (FR-005)
  • INV-C (never silent): every variant maps to a non-empty eligibility slug; run-id="" always co-occurs with a named slug. (FR-003)
  • INV-D (output boundary): main() writes ONLY run-id + eligibility; never complete/missing/coverage. (C-003)
  • INV-E (no hard-exit on no-source): NoEligibleSource/NotAMainSource/NoFallback exit 0 with an empty run-id; only malformed input raises. (NFR-004 — reconcile stays the fail-closed terminus)
  • INV-F (dispatch): workflow_dispatch → NoFallback, no ledger query attempted. (FR-006)

State transitions

None — a pure function; no lifecycle, no persisted state.