Skip to content

feat: quality report — GET /api/report and jig report (U2, U3) - #24

Merged
Steel-tech merged 5 commits into
mainfrom
feat/report-api
Sep 28, 2026
Merged

Steel-tech merged 5 commits into
mainfrom
feat/report-api

Conversation

@Steel-tech

@Steel-tech Steel-tech commented Sep 28, 2026 •

Copy link
Copy Markdown
Contributor

Implements U2 of the factory-quality plan in #21 (docs/plans/2026-09-28-001-feat-factory-quality-followups-plan.md): Store.Report behind GET /api/report (R2, R3, R4, R6, R15; KTD1, KTD3, KTD4). It is read-only and has no migration. This PR also carries U3, the jig report CLI over it (see the U3 section below).

Endpoint

GET /api/report?since=<RFC3339>&until=<RFC3339>. The window is [since, until). until defaults to now and since to seven days before until. A malformed value returns 400 invalid_query_parameter. since >= until returns 400 invalid_report_window. Duration parsing (7d) is left to U3's CLI.

Response (controlplane.Report, kept next to QueueView/FleetView because that is where the repo's read-model views live):

since, until, observed_at
jobs      {total, accepted, accepted_unpublished, failed, cancelled}
accepts   {clean, clean_ci_green, clean_no_ci_wait, person_fixed, unverified}
publish   {eligible, published, held, not_attempted, failed, unreadable, failed_codes{}, held_rate}
ci        {waited, first_pass_green,
           repair  {entered, entry_rate, succeeded, success_rate, rounds, stop_codes{}},
           reruns  {attempts, reruns, flaky_passes},
           revisit {ci_timeouts, ci_timeout_rate, retry_still_red, retry_still_red_rate}}
spend     {total_usd, unmetered_sends, per_clean_accept_usd,
           by_outcome{clean_accept|person_fixed|accepted_unverified|held|unpublished|failed|cancelled: {jobs, cost_usd, unmetered_sends}}}
unreadable_results

Counts are always numbers. A rate or per-accept figure is null when its denominator is 0, because 0 there would look like a measurement (for example, $ spent with no clean accept).

Definitions

Metric Definition
In window Job state is terminal (accepted, accepted_unpublished, failed, cancelled) and updated_at ∈ [since, until)
Clean accept Job accepted and one of: (a) green CI on a head jig pushed, meaning the ci record's remote_ref equals the proof ref or a round's head_after; or (b) the frozen definition does not wait for CI. Read from the ledger, never from the worker's result
Person-fixed accepted with the green ci record on any other head
Unverified accepted, but the ledger cannot classify it: the snapshot no longer parses, or the definition waits for CI and there is no ci record. Never clean
Publish outcome Accepted jobs count as published (the R12 ledger rule). accepted_unpublished jobs are classified from the latest attempt's publish: the "held" marker or a held summary is held; "not_attempted" is not_attempted; a failed summary is failed (by code); anything else is unreadable. held_rate = held / eligible
Waited for CI Latest attempt of an eligible job whose definition waits for CI and has a proof record, meaning publish reached the CI wait. This is the denominator for entry, timeout and retry-signal rates
Summaries publish_history (oldest first, U8) followed by the current publish summary. Each stop code, round and re-run counts once per attempt
Repair entered The ledger has ≥1 pushed round, or some summary carries ci_repairs (this catches a round that stopped before pushing)
Repair succeeded The last summary carrying ci_repairs is published. Exhausted and then publish-retried to green counts as entered, unsuccessful, with its stop code
Rounds Rows in publish_ci_repairs (KTD4)
Re-runs ci_reruns entries (U4's pinned name), de-duplicated per attempt by attempt number. flaky_passes = attempts with a re-run whose outcome is passed
First-pass green Clean CI-green accepts with no repair and no re-run. A pass after a re-run is never counted here (R6)
ci_timeout rate (KTD1) Waited attempts where any summary has code ci_timeout, divided by waited
Retry-repair signal (KTD1) Waited attempts where a red summary is followed directly by a red summary. Every summary after the first is a publish-only retry. "Red" = not published and (code ci_failed or a ci_failures entry with verdict fail), so a timeout is not red
Spend SUM(json_extract(CAST(payload AS TEXT), '$.payload.cost')) and $.payload.unmetered_sends over agent_end events of every attempt of each window job. A missing field adds 0
Spend per clean accept Total spend / clean accepts. Held, person-fixed, failed and unverified jobs stay out of the denominator, and each has its own spend bucket. Buckets add up to the totals
unreadable_results Eligible jobs whose latest result is not JSON, has a missing or unknown publish, or has a history/repair/re-run entry that does not decode

R4: person_fixed and clean_no_ci_wait are the two things jig cannot vouch for. The report makes no post-merge claim.

Verification

  • just check passes (format, vet, boundary, definitions, full test suite, build).
  • report_test.go seeds ledger rows directly, including publish_history, ci_reruns and unmetered_sends. U1/U4/U8 are not needed. It asserts:
    • an exact whole-Report match over 14 in-window jobs, one per outcome, plus jobs before since, exactly at until, and still active;
    • unprovable accepts are never clean;
    • an empty window gives zeros and nulls;
    • the route matches Store.Report, and the default window is correct;
    • bad windows return 400;
    • unreadable classification, and signals that appear only in history.

Mutation results (26/27 killed)

Each mutant compiled (go vet), and the Report tests were run against it. Every mutant was killed except one:

  • window: >=→>, <→<=, terminal filter dropped;
  • clean accept: any green = clean, repair heads not counted as jig's, no-CI-wait not clean, missing ci record counted clean;
  • counted once: re-runs not de-duplicated, repair outcome taken from the wrong summary, rounds not from the ledger;
  • unreadable: bad history entry ignored, unknown marker accepted, unknown state accepted, missing publish accepted;
  • spend: MAX for SUM, any event type, latest attempt only, unmetered not summed, per-accept over clean spend only;
  • signals: timeout read from current only, still-red ignoring the predecessor, red ignoring failures, waited without proof, first-pass including flaky, flaky ignoring outcome, held summary not held.

Survived (equivalent): dropping the latest-attempt filter from the ledger subquery. The Go side already keys ledger rows by latest attempt id, so the SQL filter only narrows the read.

Not tested / known gaps

  • Spend recorded before U1 lands only covers phases that passed, so historical totals undercount until U1 merges.
  • The four report queries run without a shared transaction. This follows the store's other multi-query reads such as JobDetail, because _txlock=immediate would take the write lock. A job that finishes between queries can skew one report slightly.
  • No volume or performance test. The plan adds an events(type) index only if realistic volume shows a need.
  • Real U4/U8 payloads have not been exercised end to end. The tests use the pinned field names.

U3 — jig report CLI

jig report [--server <url>] [--since 7d|<RFC3339>] [--until <RFC3339>] [--json] (R3, R4). Files: cmd/jig/report.go, cmd/jig/report_test.go, cmd/jig/main.go (dispatch and usage), README.md (new "Measuring the factory" section).

  • --since takes a positive duration back from now (7d, 24h, 90m: Go units plus whole days) or RFC3339. It is converted to RFC3339 before the call. --until is RFC3339 only. Omitted flags fall through to the endpoint's defaults.
  • --json prints the response body verbatim (decoded as json.RawMessage, never re-encoded).
  • Prose output is one short table per section: Jobs, Accepts, Publish, CI repair (including the KTD1 revisit rates), Re-runs, Spend. Null rates print n/a. Two lines always follow:
    • the R4 caveat: jig does not watch CI after accept, so there is no post-merge CI rate, and person-fixed and non-waiting accepts are counted separately;
    • spend: the unmetered-send count, plus the note that spend recorded by jig v0.2.0 or earlier omits phases that did not pass. When unmetered sends are above 0 the line leads with "Spend undercounts".
  • Exit codes follow def.go: 0 on success, 1 when the server is unreachable or rejects the request, 2 on usage (an unparseable --since/--until or a stray positional; usage is printed).

Verification

  • just check passes.
  • The tests run against a live jig serve (startServe) with the ledger seeded by SQL. They cover:
    • the prose names every section and both caveat lines, n/a for empty-denominator rates, and $/clean accept;
    • --json deep-equals GET /api/report for the same window, ignoring observed_at;
    • --since 7d/90m/30m and an RFC3339 value select the right jobs, and 7d is sent as now−7d;
    • bad windows exit 2 with usage;
    • an unreachable server exits 1.
  • Mutation (4/4 killed, each compiled): days multiplier 24h→1h; --json branch disabled; bad --since exit 2→1; R4 caveat line removed.

Known gaps

  • The zero-unmetered wording of the spend line has no test.
  • Spend buckets print in alphabetical order, not the server's bucket order.

🤖 Generated with Claude Code

Steel-tech and others added 4 commits September 28, 2026 00:27
…(U2)

Store.Report aggregates one [since, until) window of terminal jobs:
jobs by state, clean vs person-fixed accepts from the publish ledger,
publish outcomes and held rate, CI repair entry/success/rounds/stop
codes, flaky re-runs, KTD1's two revisit rates, and agent_end spend per
outcome bucket with spend per clean accept. It reads publish_history
(U8), ci_reruns (U4) and unmetered_sends (U1) when present and tolerates
their absence. Read-only, no migration.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…signals

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… sums

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Sep 28, 2026 •

Copy link
Copy Markdown

Warning

Review limit reached

Next included review available in 37 minutes.

Check out review usage here.

View limit details

Limit details: You’ve used the included review currently available.

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

Learn how review limits work.

Review configuration:

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Advanced

Run ID: 42188221-a2e7-46df-9ccb-3fc98dde2fea

📥 Commits

Reviewing files that changed from the base of the PR and between 8c542bb and f5db08f.

📒 Files selected for processing (7)
  • README.md
  • cmd/jig/main.go
  • cmd/jig/report.go
  • cmd/jig/report_test.go
  • internal/controlplane/http.go
  • internal/controlplane/report.go
  • internal/controlplane/report_test.go

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

`jig report [--server] [--since 7d|<RFC3339>] [--until <RFC3339>] [--json]`
reads GET /api/report. A duration --since converts to RFC3339 against now;
--json prints the API object verbatim; the prose form is one short table
per section (jobs, accepts, publish, CI repair, re-runs, spend), prints
null rates as n/a, and states in one line each what jig cannot see (R4)
and the unmetered-send count. Exit codes follow def.go: 0, 1 when the
control plane is unreachable or rejects, 2 on usage.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@Steel-tech Steel-tech changed the title feat(controlplane): quality report over the ledger — GET /api/report (U2) feat: quality report — GET /api/report and jig report (U2, U3) Sep 28, 2026
@Steel-tech
Steel-tech merged commit 1da3e80 into main Sep 28, 2026
5 checks passed
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.

1 participant