Skip to content

Commit e5cdcbf

Browse files
committed
fix: resolve all tsc strict mode errors for CI build
- Add BillingRow type for rows with billing fields (date, grossAmount, etc.) - Use proper type narrowing in computeSummary with isBillingRow guard - Cast CSV string values to their literal union types in csv-parser - Use specific row types instead of Record<string, unknown> in tests - Chart components narrow to BillingRow since they only handle billing data
1 parent 4008ff4 commit e5cdcbf

File tree

6 files changed

+50
-39
lines changed

6 files changed

+50
-39
lines changed

src/components/charts/CostBreakdownChart.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import { groupBy, sumBy, timeBucket as bucketRows } from '../../lib/aggregation'
77
import { buildColorMap } from '../../lib/chart-theme';
88
import { formatDisplayValue, formatCompact, bucketKeyToTimestamp } from '../../lib/formatters';
99
import type { MetricOption } from '../../lib/report-schema';
10-
import type { AnyReportRow } from '../../lib/types';
10+
import type { AnyReportRow, BillingRow } from '../../lib/types';
1111
import styles from './Charts.module.css';
1212

1313
interface CostBreakdownChartProps {
@@ -28,7 +28,7 @@ export function CostBreakdownChart({ stackField = 'model', metricOptions }: Cost
2828
const options = useMemo((): Highcharts.Options | null => {
2929
if (!activeReport) return null;
3030

31-
const allRows = visibleRows as AnyReportRow[];
31+
const allRows = visibleRows as BillingRow[];
3232
const rows = activeMetric.rowFilter
3333
? allRows.filter((r) => activeMetric.rowFilter!(r as unknown as Record<string, unknown>))
3434
: allRows;

src/components/charts/TimeSeriesChart.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import { humanizeColumn, formatDisplayValue, formatCompact, bucketKeyToTimestamp
88
import { buildColorMap } from '../../lib/chart-theme';
99
import { getStoredValue, setStoredValue, STORAGE_KEYS } from '../../lib/local-storage';
1010
import type { MetricOption } from '../../lib/report-schema';
11-
import type { AnyReportRow } from '../../lib/types';
11+
import type { AnyReportRow, BillingRow } from '../../lib/types';
1212
import styles from './Charts.module.css';
1313

1414
const LINE_MODES = ['raw', 'rolling', 'cumulative'] as const;
@@ -80,7 +80,7 @@ export function TimeSeriesChart({ metricOptions }: { metricOptions?: MetricOptio
8080
const options = useMemo((): Highcharts.Options | null => {
8181
if (!activeReport) return null;
8282

83-
const allRows = visibleRows as AnyReportRow[];
83+
const allRows = visibleRows as BillingRow[];
8484
const rows = activeMetric.rowFilter
8585
? allRows.filter((r) => activeMetric.rowFilter!(r as unknown as Record<string, unknown>))
8686
: allRows;

src/lib/aggregation.ts

Lines changed: 23 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import type { AnyReportRow, TimeBucket, ReportSummary } from './types';
1+
import type { AnyReportRow, BillingRow, TimeBucket, ReportSummary, UsageReportRow, TokenUsageRow } from './types';
22

33
/** Group rows by a column value, returning a map of group key → rows */
44
export function groupBy<T extends AnyReportRow>(
@@ -53,7 +53,7 @@ export function getTimeBucketKey(dateStr: string, bucket: TimeBucket): string {
5353
}
5454

5555
/** Group rows by time bucket, returning sorted entries */
56-
export function timeBucket<T extends AnyReportRow>(
56+
export function timeBucket<T extends BillingRow>(
5757
rows: T[],
5858
bucket: TimeBucket,
5959
): Map<string, T[]> {
@@ -92,9 +92,15 @@ export function topN<T extends AnyReportRow>(
9292
return ranked.slice(0, n);
9393
}
9494

95+
/** Type guard for rows with billing fields */
96+
function isBillingRow(row: AnyReportRow): row is BillingRow {
97+
return 'date' in row && 'grossAmount' in row;
98+
}
99+
95100
/** Compute summary metrics for a set of rows */
96101
export function computeSummary(rows: AnyReportRow[]): ReportSummary {
97-
const dates = rows.map((r) => r.date).filter(Boolean).sort();
102+
const billingRows = rows.filter(isBillingRow);
103+
const dates = billingRows.map((r) => r.date).filter(Boolean).sort();
98104
const users = new Set<string>();
99105
const organizations = new Set<string>();
100106
const models = new Set<string>();
@@ -109,30 +115,31 @@ export function computeSummary(rows: AnyReportRow[]): ReportSummary {
109115
let totalStorageGBH = 0;
110116
let totalTokens = 0;
111117

112-
for (const row of rows) {
118+
for (const row of billingRows) {
113119
totalGrossAmount += row.grossAmount;
114120
totalNetAmount += row.netAmount;
115121
totalDiscountAmount += row.discountAmount;
116122
totalQuantity += row.quantity;
117123

118-
if ('username' in row && row.username) users.add(row.username as string);
119-
if ('organization' in row && row.organization) organizations.add(row.organization as string);
120-
if ('model' in row && row.model) models.add(row.model as string);
121-
if ('repository' in row && (row as Record<string, unknown>).repository) repositories.add((row as Record<string, unknown>).repository as string);
122-
if ('product' in row && (row as Record<string, unknown>).product) products.add((row as Record<string, unknown>).product as string);
124+
if ('username' in row && row.username) users.add(row.username);
125+
if ('organization' in row && row.organization) organizations.add(row.organization);
126+
if ('model' in row) models.add(row.model);
127+
const usageRow = row as UsageReportRow;
128+
if ('repository' in row && usageRow.repository) repositories.add(usageRow.repository);
129+
if ('product' in row && row.product) products.add(String(row.product));
123130

124-
// Accumulate unit-type-specific totals
131+
// Accumulate unit-type-specific totals for usage reports
125132
if ('unitType' in row) {
126-
const unitType = (row as Record<string, unknown>).unitType;
127-
if (unitType === 'minutes') totalMinutes += row.quantity;
128-
if (unitType === 'gigabyte-hours') totalStorageGBH += row.quantity;
133+
const u = row as UsageReportRow;
134+
if (u.unitType === 'minutes') totalMinutes += u.quantity;
135+
if (u.unitType === 'gigabyte-hours') totalStorageGBH += u.quantity;
129136
}
130137

131138
// Accumulate token totals
132139
if ('totalInputTokens' in row) {
133-
const r = row as Record<string, number>;
134-
totalTokens += (r.totalInputTokens ?? 0) + (r.totalOutputTokens ?? 0)
135-
+ (r.totalCacheCreationTokens ?? 0) + (r.totalCacheReadTokens ?? 0);
140+
const t = row as TokenUsageRow;
141+
totalTokens += (t.totalInputTokens ?? 0) + (t.totalOutputTokens ?? 0)
142+
+ (t.totalCacheCreationTokens ?? 0) + (t.totalCacheReadTokens ?? 0);
136143
}
137144
}
138145

src/lib/csv-parser.test.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -323,7 +323,7 @@ describe('parseCSV — Token Usage (real file)', () => {
323323

324324
it('does NOT have aicQuantity or aicGrossAmount fields', () => {
325325
const report = parseCSV(csv, 'Token.Usage.Report.csv');
326-
const row = report.rows[0] as Record<string, unknown>;
326+
const row = report.rows[0] as TokenUsageRow;
327327
expect(row).not.toHaveProperty('aicQuantity');
328328
expect(row).not.toHaveProperty('aicGrossAmount');
329329
});
@@ -523,7 +523,7 @@ describe('parseCSV — GHAS Active Committers (real file)', () => {
523523

524524
it('does NOT have billing columns (no grossAmount, netAmount, etc.)', () => {
525525
const report = parseCSV(csv, 'ghas.csv');
526-
const row = report.rows[0] as Record<string, unknown>;
526+
const row = report.rows[0] as GhasActiveCommittersRow;
527527
expect(row).not.toHaveProperty('grossAmount');
528528
expect(row).not.toHaveProperty('netAmount');
529529
expect(row).not.toHaveProperty('quantity');
@@ -567,7 +567,7 @@ describe('cross-report schema validation', () => {
567567
it('token usage has token fields that premium request lacks', () => {
568568
const tuCsv = loadExample('Token.Usage.Report.csv');
569569
const tuReport = parseCSV(tuCsv, 'tu.csv');
570-
const tuRow = tuReport.rows[0] as Record<string, unknown>;
570+
const tuRow = tuReport.rows[0] as TokenUsageRow;
571571

572572
expect(tuRow).toHaveProperty('totalInputTokens');
573573
expect(tuRow).toHaveProperty('totalOutputTokens');
@@ -579,7 +579,7 @@ describe('cross-report schema validation', () => {
579579
it('premium request has AIC fields that token usage lacks', () => {
580580
const prCsv = loadExample('premiumRequestUsageReport_1_c6fca30f0acd458098a95808eaf43399.csv');
581581
const prReport = parseCSV(prCsv, 'pr.csv');
582-
const prRow = prReport.rows[0] as Record<string, unknown>;
582+
const prRow = prReport.rows[0] as PremiumRequestRow;
583583

584584
expect(prRow).toHaveProperty('aicQuantity');
585585
expect(prRow).toHaveProperty('aicGrossAmount');

src/lib/csv-parser.ts

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,10 @@ import type {
66
GhasActiveCommittersRow,
77
ParsedReport,
88
ReportType,
9+
CopilotProduct,
10+
PremiumRequestSku,
11+
UsageProduct,
12+
UsageUnitType,
913
} from './types';
1014
import { REPORT_TYPES } from './types';
1115

@@ -47,11 +51,11 @@ function mapPremiumRequestRow(raw: Record<string, string>): PremiumRequestRow {
4751
return {
4852
date: raw['date'] ?? '',
4953
username: raw['username'] ?? '',
50-
product: raw['product'] ?? '',
51-
sku: raw['sku'] ?? '',
54+
product: (raw['product'] ?? '') as CopilotProduct,
55+
sku: (raw['sku'] ?? '') as PremiumRequestSku,
5256
model: raw['model'] ?? '',
5357
quantity: parseNum(raw['quantity']),
54-
unitType: raw['unit_type'] ?? '',
58+
unitType: (raw['unit_type'] ?? '') as 'requests' | 'ai-units',
5559
appliedCostPerQuantity: parseNum(raw['applied_cost_per_quantity']),
5660
grossAmount: parseNum(raw['gross_amount']),
5761
discountAmount: parseNum(raw['discount_amount']),
@@ -70,11 +74,11 @@ function mapTokenUsageRow(raw: Record<string, string>): TokenUsageRow {
7074
return {
7175
date: raw['date'] ?? '',
7276
username: raw['username'] ?? '',
73-
product: raw['product'] ?? '',
74-
sku: raw['sku'] ?? '',
77+
product: (raw['product'] ?? '') as CopilotProduct,
78+
sku: (raw['sku'] ?? '') as PremiumRequestSku,
7579
model: raw['model'] ?? '',
7680
quantity: parseNum(raw['quantity']),
77-
unitType: raw['unit_type'] ?? '',
81+
unitType: (raw['unit_type'] ?? '') as 'requests' | 'ai-units',
7882
appliedCostPerQuantity: parseNum(raw['applied_cost_per_quantity']),
7983
grossAmount: parseNum(raw['gross_amount']),
8084
discountAmount: parseNum(raw['discount_amount']),
@@ -94,10 +98,10 @@ function mapTokenUsageRow(raw: Record<string, string>): TokenUsageRow {
9498
function mapUsageReportRow(raw: Record<string, string>): UsageReportRow {
9599
return {
96100
date: raw['date'] ?? '',
97-
product: raw['product'] ?? '',
101+
product: (raw['product'] ?? '') as UsageProduct,
98102
sku: raw['sku'] ?? '',
99103
quantity: parseNum(raw['quantity']),
100-
unitType: raw['unit_type'] ?? '',
104+
unitType: (raw['unit_type'] ?? '') as UsageUnitType,
101105
appliedCostPerQuantity: parseNum(raw['applied_cost_per_quantity']),
102106
grossAmount: parseNum(raw['gross_amount']),
103107
discountAmount: parseNum(raw['discount_amount']),

src/lib/types.ts

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -170,8 +170,7 @@ export type GhasActiveCommittersCsvHeader =
170170
// ─── Parsed Row Interfaces ─────────────────────────────────────────────────────
171171

172172
/** Shared columns across Premium Request and Token Usage reports */
173-
interface BaseCopilotReportRow {
174-
/** ISO date string (YYYY-MM-DD) */
173+
interface BaseCopilotReportRow { /** ISO date string (YYYY-MM-DD) */
175174
date: string;
176175
/** GitHub username */
177176
username: string;
@@ -224,8 +223,7 @@ export interface TokenUsageRow extends BaseCopilotReportRow {
224223
}
225224

226225
/** General Usage Report row (Actions, Copilot seats, LFS, Packages) */
227-
export interface UsageReportRow {
228-
/** ISO date string (YYYY-MM-DD), first of month */
226+
export interface UsageReportRow { /** ISO date string (YYYY-MM-DD), first of month */
229227
date: string;
230228
/** Product category */
231229
product: UsageProduct;
@@ -256,8 +254,7 @@ export interface UsageReportRow {
256254
}
257255

258256
/** GHAS Active Committers report row */
259-
export interface GhasActiveCommittersRow {
260-
/** GitHub username */
257+
export interface GhasActiveCommittersRow { /** GitHub username */
261258
userLogin: string;
262259
/** Combined "org/repo" string that needs splitting for org vs repo */
263260
organizationRepository: string;
@@ -286,7 +283,10 @@ export interface ParsedReport<T = PremiumRequestRow | TokenUsageRow | UsageRepor
286283
dateRange: { start: string; end: string };
287284
}
288285

289-
export type AnyReportRow = PremiumRequestRow | TokenUsageRow | UsageReportRow | GhasActiveCommittersRow;
286+
/** Report rows that have billing fields (date, grossAmount, netAmount, etc.) */
287+
export type BillingRow = PremiumRequestRow | TokenUsageRow | UsageReportRow;
288+
289+
export type AnyReportRow = BillingRow | GhasActiveCommittersRow;
290290

291291
/** Groupable columns vary by report type */
292292
export const GROUPABLE_COLUMNS = {

0 commit comments

Comments
 (0)