Skip to content

feat(reporters): surface modeled tables in the CI comment - #193

Merged
veksen merged 1 commit into
mainfrom
feat-modeled-tables-banner
Jul 27, 2026
Merged

veksen merged 1 commit into
mainfrom
feat-modeled-tables-banner

Conversation

@veksen

@veksen veksen commented Jul 27, 2026

Copy link
Copy Markdown
Member

Goal

A developer reading a Query Doctor PR comment should be able to tell a measured cost from an estimated one. Costs are planned against a snapshot of production statistics; a table the snapshot has never seen has no real row count, so the synthesizer estimates one. Those estimates are plausible, not measured, and a query against such a table can pass the gate for the wrong reason.

This is the reporting half of Site#3420. The data half merged in #184. The MCP tools already carry the caveat; this brings the CI comment in line. No follow-up is planned after this — see the note at the bottom for a separate item this uncovered.

What

Before: a PR touching a table the snapshot doesn't cover showed its cost with no indication the number was estimated.

After: the comment opens with a note naming those tables.

Note

1 table(s) in your schema aren't covered by the statistics you loaded, so their costs are modeled, not verified against real data: public.invoices. A query touching one isn't a clean pass.

Nothing renders when the snapshot covers the whole schema. Nothing gates on it — the severity is NOTE, not CAUTION, so it must not read as a blocking error.

How

Read src/reporters/github/github.ts first. buildModeledTablesNotice(ctx) turns ReportContext.modeledTables into {count, list}, or null when the list is empty, and the template renders on that single truthy check. The wording and the ten-name and N more cap are copied from modeledTablesWarning in Site's packages/mcp-server/src/apply-stats.ts, so the CI comment and the MCP tools tell a developer the same thing.

Two decisions worth flagging:

The field is named modeledTablesNotice, not modeledTables. report() renders with {...ctx, ...viewModel}, so ctx.modeledTables is already in template scope. Reusing the key would shadow a string[] with an object, which works only as long as viewModel keeps spreading second.

buildViewModel has two return statements, and the early one covers runs with no baseline. A field added to one and not the other vanishes silently on exactly those runs — it compiles, it type-checks, and it produces the false all-clear this work exists to remove. Both returns carry the field, and a test pins the no-baseline path on its own. The same shape of bug hit the Site side once already (ac5c73c72).

src/reporters/reporter.ts has a one-line doc fix: the comment on modeledTables claimed the field wasn't rendered yet.

Tests

Eight tests in github.test.ts. On the view model: the comparison path, the no-baseline path, the ten-name cap with and 3 more, an empty list, and an absent list. On the template: the note renders with a NOTE severity and never CAUTION or WARNING, it renders on a run with no baseline, and it renders nothing when there are no modeled tables.

The existing snapshots are unchanged rather than regenerated. Neither sets modeledTables, so nothing new renders for them; leaving them untouched proves this didn't disturb unrelated output.

Full suite passes (344 tests, 35 files). tsc --noEmit is clean.

Beyond the suite, I rendered the real success.md.j2 through the real buildViewModel, using the same spread order report() uses, across four contexts: with a baseline, without one, with 13 tables, and with none. Output matched in all four.

Not verified: no live CI run against a branch that adds an uncovered table. Everything upstream of ReportContext.modeledTables is trusted on #184 having merged, not observed here. Worth a qa-setup-ci pass against a throwaway migration before this is relied on.

Not in this PR

Statistics's optional fifth constructor argument: synthesis only runs when a caller passes currentSchema, and Statistics.fromPostgres can't pass it, so some paths never synthesize at all. Core 0.16.0 is published and this repo now calls the constructor positionally, so changing that shape is a coordinated break across two repos. It should be done deliberately and separately.

The synthesizer estimates row counts for tables the production snapshot
never covered, so any cost touching one is modeled rather than measured.
The MCP tools already say so. The CI comment did not, leaving no way to
tell a verified cost from an estimate (Site#3420).

Render a non-blocking NOTE naming those tables. The wording and the
ten-name cap come from Site's `modeledTablesWarning`, so the two surfaces
stay consistent.

`buildViewModel` returns from two places, and the early return handles
runs with no baseline. Both carry the new field, and a test pins the
non-comparison path. Existing snapshots set no modeled tables, so they
are unchanged.

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

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Query Doctor Analysis

3 queries analyzed

0 regressed · 0 improved · 0 new · 0 removed

2 pre-existing issues

Using assumed statistics (10000000 rows/table). For better results, sync production stats.

More detail → get_ci_run({ runId: "019fa48c-cf18-7970-b05b-acf99dba5223" }) · view run · docs

@veksen
veksen merged commit a35e0ab into main Jul 27, 2026
6 checks passed
@veksen
veksen deleted the feat-modeled-tables-banner branch July 27, 2026 20:13
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