diff --git a/openspec.yaml b/openspec.yaml index b96c887..ce36a51 100644 --- a/openspec.yaml +++ b/openspec.yaml @@ -742,6 +742,16 @@ paths: from: { type: string } to: { type: string } grain: { type: string, enum: [daily, session] } + series: + type: array + description: Per-day breakdown for the requested range (daily grain only). Missing days are zero-filled. + items: + type: object + properties: + date: { type: string, pattern: '^\d{4}-\d{2}-\d{2}$' } + lines_added: { type: integer } + lines_deleted: { type: integer } + ratio: { type: number, nullable: true } sessions: type: array items: @@ -801,6 +811,15 @@ paths: from: { type: string } to: { type: string } grain: { type: string, enum: [daily, session] } + series: + type: array + description: Per-day breakdown for the requested range (daily grain only). Missing days are zero-filled. + items: + type: object + properties: + date: { type: string, pattern: '^\d{4}-\d{2}-\d{2}$' } + switch_count: { type: integer } + rapid_switch_count: { type: integer } sessions: type: array items: diff --git a/src/services/metrics.service.js b/src/services/metrics.service.js index 1d8d5fe..62941ca 100644 --- a/src/services/metrics.service.js +++ b/src/services/metrics.service.js @@ -77,10 +77,30 @@ export async function getChurn({ userId, from, to, grain }) { added += Number(row.lines_added) || 0; deleted += Number(row.lines_deleted) || 0; } + + const byDate = new Map(); + for (const row of rows) { + const key = row.date instanceof Date ? row.date.toISOString().slice(0, 10) : String(row.date); + byDate.set(key, { + lines_added: Number(row.lines_added) || 0, + lines_deleted: Number(row.lines_deleted) || 0, + }); + } + const series = enumerateDates(from, to).map((date) => { + const entry = byDate.get(date) ?? { lines_added: 0, lines_deleted: 0 }; + return { + date, + lines_added: entry.lines_added, + lines_deleted: entry.lines_deleted, + ratio: computeRatio(entry.lines_added, entry.lines_deleted), + }; + }); + return { ratio: computeRatio(added, deleted), total_lines_added: added, total_lines_deleted: deleted, + series, definition: CHURN_DEFINITION, from, to, @@ -88,6 +108,16 @@ export async function getChurn({ userId, from, to, grain }) { }; } +function enumerateDates(from, to) { + const start = new Date(`${from}T00:00:00.000Z`); + const end = new Date(`${to}T00:00:00.000Z`); + const dates = []; + for (let d = start; d <= end; d = new Date(d.getTime() + 24 * 60 * 60 * 1000)) { + dates.push(d.toISOString().slice(0, 10)); + } + return dates; +} + export async function getContextSwitching({ userId, from, to, grain, topN }) { if (grain === 'session') { const rows = await sessionRowsForRange(userId, from, to); @@ -107,10 +137,25 @@ export async function getContextSwitching({ userId, from, to, grain, topN }) { switchCount += Number(row.editor_switch_count) || 0; rapidCount += Number(row.rapid_switch_count) || 0; } + + const byDate = new Map(); + for (const row of rows) { + const key = row.date instanceof Date ? row.date.toISOString().slice(0, 10) : String(row.date); + byDate.set(key, { + switch_count: Number(row.editor_switch_count) || 0, + rapid_switch_count: Number(row.rapid_switch_count) || 0, + }); + } + const series = enumerateDates(from, to).map((date) => { + const entry = byDate.get(date) ?? { switch_count: 0, rapid_switch_count: 0 }; + return { date, switch_count: entry.switch_count, rapid_switch_count: entry.rapid_switch_count }; + }); + return { switch_count: switchCount, rapid_switch_count: rapidCount, top_files: mergeTopFiles(rows, topN), + series, definition: SWITCH_DEFINITION, from, to,