Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions cspell.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,9 @@
"btcbb",
"burndown",
"bxapp",
"canonify",
"capslock",
"cavecrew",
"chartjs",
"chatmode",
"chatmodes",
Expand Down Expand Up @@ -49,20 +52,33 @@
"gantt",
"ghaw",
"glpat",
"goldmark",
"harness",
"harnesses",
"interestingness",
"jukka",
"knip",
"kurkela",
"leitner",
"linecap",
"linejoin",
"lookback",
"markdownify",
"metas",
"metachars",
"metaprogramming",
"monokai",
"multiset",
"nathachai",
"nodata",
"nullglob",
"onedrive",
"opencode",
"optim",
"osascript",
"outonly",
"pousr",
"precomputation",
"prereqs",
"prodreview",
"promptfile",
Expand All @@ -82,14 +98,18 @@
"sidechain",
"sparkline",
"tamas",
"thongniran",
"todoread",
"tokenless",
"toolcall",
"toolsmith",
"topbar",
"treemap",
"tseslint",
"undercount",
"unparseable",
"unreviewed",
"unsandboxed",
"upskilling",
"visualbasic",
"vitest",
Expand Down
5 changes: 4 additions & 1 deletion docs/content/measure/output.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,10 @@ The Output page shows your **Code Output** -- how much code your AI assistants h
The Code Output tab measures how much code your AI assistants have generated:

- **AI-Generated LoC** -- Total estimated lines of code across all sessions
- **Net AI LoC** -- Lines the AI added minus lines the AI removed, across all sessions (can be negative)

The **Daily Production** chart shows lines of code per day as a bar chart. Below it, breakdowns show production split **by language** (TypeScript, CSS, Python, etc.), **by workspace**, **by model**, and **by harness**.
The **Daily AI Code Output** chart shows the net new lines of code the AI assistant wrote per day as a bar chart (it aggregates by week or month over longer ranges). Each edit is compared against the previous version of the file, so only added lines are counted -- re-saving an unchanged file or rewriting the same lines is not double-counted. Below it, breakdowns show output split **by language** (TypeScript, CSS, Python, etc.), **by workspace**, **by model**, and **by harness**.

The **Net Code Output** charts complement gross output by accounting for deletions. The default chart diverges added lines (above zero) against removed lines (below zero) with a net line overlaid, so a day where the assistant deleted or rewrote more than it added dips below zero. **Net by Model**, **Net by Workspace**, and **Net by Harness** tabs break the same net figure (added minus removed) down by dimension. This reflects the lasting footprint of AI edits on your files rather than just gross volume.

Time range selectors let you view the last 7 days, 4 weeks, 3 months, 6 months, or all time.
2 changes: 1 addition & 1 deletion scripts/benchmark-reload-stability.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ type WorkerPayload = {
result: {
workspaces: Array<[string, { id: string; name: string; path: string }]>;
sessions: import('../src/core/types').Session[];
editLocIndex: Array<[string, Array<[string, number]>]>;
editLocIndex: Array<[string, Array<[string, { added: number; removed: number }]>]>;
};
};

Expand Down
7 changes: 4 additions & 3 deletions src/core/analyzer-base.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,15 @@
/* Base class for analyzer modules -- provides shared filtering logic */

import { Session, SessionRequest, DateFilter } from './types';
import { EditLocIndex } from './edit-loc-diff';
import { toDateStr } from './helpers';

export class AnalyzerBase {
protected readonly sessions: Session[];
protected readonly editLocIndex: Map<string, Map<string, number>>;
protected readonly editLocIndex: EditLocIndex;
protected readonly requestSessionMap: Map<SessionRequest, Session>;

constructor(sessions: Session[], editLocIndex: Map<string, Map<string, number>>, sharedMap?: Map<SessionRequest, Session>) {
constructor(sessions: Session[], editLocIndex: EditLocIndex, sharedMap?: Map<SessionRequest, Session>) {
this.sessions = sessions;
this.editLocIndex = editLocIndex;
if (sharedMap) {
Expand Down Expand Up @@ -57,7 +58,7 @@ export class AnalyzerBase {
protected requestLoc(r: SessionRequest): number {
let loc = r.aiCode.reduce((s, b) => s + b.loc, 0);
const eMap = this.editLocIndex.get(r.requestId);
if (eMap) for (const v of eMap.values()) loc += v;
if (eMap) for (const v of eMap.values()) loc += v.added;
return loc;
}

Expand Down
3 changes: 2 additions & 1 deletion src/core/analyzer-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@

import * as fs from 'fs';
import * as path from 'path';
import { EditLocIndex } from './edit-loc-diff';
import {
Session, DateFilter, Workspace, AntiPattern, OccurrenceDetail,
ConfigHealthData, WorkspaceConfigHealth,
Expand All @@ -32,7 +33,7 @@
export class ConfigAnalyzer extends AnalyzerBase {
private workspaces: Map<string, Workspace>;

constructor(sessions: Session[], editLocIndex: Map<string, Map<string, number>>, workspaces: Map<string, Workspace>, sharedMap?: Map<import('./types').SessionRequest, Session>) {
constructor(sessions: Session[], editLocIndex: EditLocIndex, workspaces: Map<string, Workspace>, sharedMap?: Map<import('./types').SessionRequest, Session>) {
super(sessions, editLocIndex, sharedMap);
this.workspaces = workspaces;
}
Expand Down Expand Up @@ -327,7 +328,7 @@
return patterns;
}

private computeContextProvisionByHarness(f?: DateFilter): Record<string, ContextProvisionScore> {

Check warning on line 331 in src/core/analyzer-config.ts

View workflow job for this annotation

GitHub Actions / verify

Method 'computeContextProvisionByHarness' has a complexity of 23. Maximum allowed is 20
const reqs = this.filter(f);
const sessions = this.filteredSessions(f);
const result: Record<string, ContextProvisionScore> = {};
Expand Down
2 changes: 1 addition & 1 deletion src/core/analyzer-patterns.ts
Original file line number Diff line number Diff line change
Expand Up @@ -472,7 +472,7 @@ export class PatternsAnalyzer extends AnalyzerBase {
estimatedLoc += request.aiCode.reduce((sum, block) => sum + block.loc, 0);
const editLocs = this.editLocIndex.get(request.requestId);
if (editLocs) {
for (const loc of editLocs.values()) estimatedLoc += loc;
for (const loc of editLocs.values()) estimatedLoc += loc.added;
}
}
return estimatedLoc;
Expand Down
59 changes: 38 additions & 21 deletions src/core/analyzer-production.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,16 +12,22 @@

export class ProductionAnalyzer extends AnalyzerBase {

getCodeProduction(f?: DateFilter): CodeProductionData {

Check warning on line 15 in src/core/analyzer-production.ts

View workflow job for this annotation

GitHub Actions / verify

Method 'getCodeProduction' has a complexity of 27. Maximum allowed is 20
const reqs = this.filter(f);
let totalAiLoc = 0;
let totalRemovedAiLoc = 0;
let aiBlocks = 0;
const langAi = new Map<string, number>();
const dailyAi = new Map<string, number>();
const dailyRemovedAi = new Map<string, number>();
const wsAi = new Map<string, number>();
const wsRemoved = new Map<string, number>();
const dailyWsAi = new Map<string, Map<string, number>>();
const dailyWsRemoved = new Map<string, Map<string, number>>();
const dailyModelAi = new Map<string, Map<string, number>>();
const dailyModelRemoved = new Map<string, Map<string, number>>();
const dailyHarnessAi = new Map<string, Map<string, number>>();
const dailyHarnessRemoved = new Map<string, Map<string, number>>();

for (const request of reqs) {
const day = toDateStr(request.timestamp!);
Expand Down Expand Up @@ -49,13 +55,20 @@
const model = normalizeModel(request.modelId || 'unknown');
const harness = session?.harness || 'unknown';
for (const [file, loc] of editLocs) {
totalAiLoc += loc;
this.addProductionLoc(langAi, file.split('.').pop()?.toLowerCase() || 'unknown', loc);
if (day) this.addProductionLoc(dailyAi, day, loc);
this.addWorkspaceProductionLoc(wsAi, dailyWsAi, workspaceName, day, loc);
totalAiLoc += loc.added;
totalRemovedAiLoc += loc.removed;
this.addProductionLoc(langAi, file.split('.').pop()?.toLowerCase() || 'unknown', loc.added);
if (day) {
this.addDailyGroupLoc(dailyModelAi, model, day, loc);
this.addDailyGroupLoc(dailyHarnessAi, harness, day, loc);
this.addProductionLoc(dailyAi, day, loc.added);
this.addProductionLoc(dailyRemovedAi, day, loc.removed);
}
this.addWorkspaceProductionLoc(wsAi, dailyWsAi, workspaceName, day, loc.added);
this.addWorkspaceProductionLoc(wsRemoved, dailyWsRemoved, workspaceName, day, loc.removed);
if (day) {
this.addDailyGroupLoc(dailyModelAi, model, day, loc.added);
this.addDailyGroupLoc(dailyHarnessAi, harness, day, loc.added);
this.addDailyGroupLoc(dailyModelRemoved, model, day, loc.removed);
this.addDailyGroupLoc(dailyHarnessRemoved, harness, day, loc.removed);
}
}
}
Expand All @@ -80,6 +93,7 @@
return {
summary: {
totalAiLoc, totalUserLoc: 0, totalLoc: totalAiLoc,
totalRemovedAiLoc, totalNetAiLoc: totalAiLoc - totalRemovedAiLoc,
aiBlocks, userBlocks: 0, aiRatio: 1,
locCost2010,
costPerLoc: totalAiLoc > 0 ? locCost2010 / totalAiLoc : 0,
Expand All @@ -92,31 +106,34 @@
dailyTimeline: {
labels: dayArr,
aiLoc: dayArr.map(d => dailyAi.get(d) || 0),
removedLoc: dayArr.map(d => dailyRemovedAi.get(d) || 0),
userLoc: dayArr.map(() => 0),
},
byWorkspace: {
labels: wsArr,
aiLoc: wsArr.map(w => wsAi.get(w) || 0),
userLoc: wsArr.map(() => 0),
},
dailyByWorkspace: Object.fromEntries(
Array.from(dailyWsAi.entries()).map(([ws, dm]) => [
ws, dayArr.map(d => dm.get(d) || 0),
])
),
dailyByModel: Object.fromEntries(
Array.from(dailyModelAi.entries()).map(([m, dm]) => [
m, dayArr.map(d => dm.get(d) || 0),
])
),
dailyByHarness: Object.fromEntries(
Array.from(dailyHarnessAi.entries()).map(([h, dm]) => [
h, dayArr.map(d => dm.get(d) || 0),
])
),
dailyByWorkspace: this.toDailyRecord(dailyWsAi, dayArr),
dailyRemovedByWorkspace: this.toDailyRecord(dailyWsRemoved, dayArr),
dailyByModel: this.toDailyRecord(dailyModelAi, dayArr),
dailyRemovedByModel: this.toDailyRecord(dailyModelRemoved, dayArr),
dailyByHarness: this.toDailyRecord(dailyHarnessAi, dayArr),
dailyRemovedByHarness: this.toDailyRecord(dailyHarnessRemoved, dayArr),
};
}

private toDailyRecord(
groupMap: Map<string, Map<string, number>>,
dayArr: string[],
): Record<string, number[]> {
return Object.fromEntries(
Array.from(groupMap.entries()).map(([key, dm]) => [
key, dayArr.map(d => dm.get(d) || 0),
])
);
}

private addProductionLoc(target: Map<string, number>, key: string, loc: number): void {
target.set(key, (target.get(key) || 0) + loc);
}
Expand Down
75 changes: 75 additions & 0 deletions src/core/analyzer.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import { describe, it, expect } from 'vitest';
import { Analyzer } from './analyzer';
import { Session, SessionRequest, DateFilter } from './types';
import { accumulateEditLoc, EditTimelineLike } from './edit-loc-diff';

/**
* Simulates panel.ts validateDateFilter -- this is the exact logic
Expand Down Expand Up @@ -384,7 +385,7 @@

it('harness filter propagates to getCodeProduction', () => {
const a = new Analyzer(sessions);
const filter = validateDateFilter({ harness: 'Codex' } as Record<string, unknown>);

Check warning on line 388 in src/core/analyzer.test.ts

View workflow job for this annotation

GitHub Actions / verify

This assertion is unnecessary since the receiver accepts the original type of the expression
const prod = a.getCodeProduction(filter);
// s4 has 1 request with default aiCode [{ language: 'typescript', loc: 10 }]
expect(prod.summary.totalAiLoc).toBe(10);
Expand All @@ -393,7 +394,7 @@
it('harness filter propagates to getConsumption', () => {
const a = new Analyzer(sessions);
// Filter by Local Agent — s1 has 2 requests, s3 has 3.
const filter = validateDateFilter({ harness: 'Local Agent' } as Record<string, unknown>);

Check warning on line 397 in src/core/analyzer.test.ts

View workflow job for this annotation

GitHub Actions / verify

This assertion is unnecessary since the receiver accepts the original type of the expression
const cons = a.getConsumption(filter);
expect(cons.totalRequests).toBe(5); // s1(2) + s3(3)

Expand Down Expand Up @@ -631,3 +632,77 @@
});
});
});

describe('getCodeProduction with deduplicated edit LoC', () => {
it('combines aiCode blocks with incrementally-counted edit LoC instead of summing whole-file snapshots', () => {
const URI = 'file:///proj/app.ts';
const v1 = Array.from({ length: 10 }, (_, i) => `line${i}`).join('\n');
const v2 = v1 + '\nline10';
const v3 = v2 + '\nline11';
const wholeFile = (text: string) => [{ range: { startLineNumber: 1, startColumn: 1, endLineNumber: 1_000_000, endColumn: 1 }, text }];
const timeline: EditTimelineLike = {
fileBaselines: [[`${URI}::r1`, { uri: { external: URI }, requestId: 'r1', content: v1 }]],
operations: [
{ type: 'textEdit', requestId: 'r1', uri: { external: URI }, epoch: 1, edits: wholeFile(v1) },
{ type: 'textEdit', requestId: 'r1', uri: { external: URI }, epoch: 2, edits: wholeFile(v2) },
{ type: 'textEdit', requestId: 'r1', uri: { external: URI }, epoch: 3, edits: wholeFile(v3) },
],
};
const editLocIndex = new Map<string, Map<string, { added: number; removed: number }>>();
accumulateEditLoc(timeline, editLocIndex);
// The three whole-file snapshots (10 + 11 + 12 lines) collapse to 2 newly-produced lines.
expect(editLocIndex.get('r1')?.get(URI)?.added).toBe(2);

const ts = new Date(2024, 5, 15, 10, 0, 0).getTime();
const sessions = [
makeSession({
sessionId: 's1', harness: 'Codex', creationDate: ts, workspaceName: 'proj',
requests: [makeRequest({ requestId: 'r1', timestamp: ts, aiCode: [{ language: 'typescript', loc: 5 }] })],
}),
];
const a = new Analyzer(sessions, editLocIndex);
const prod = a.getCodeProduction();
// 5 code-block LoC + 2 deduplicated edit LoC = 7 (not 5 + 33 from naive snapshot summing).
expect(prod.summary.totalAiLoc).toBe(7);
});

it('reports net = gross added minus removed and surfaces removed in the daily timeline', () => {
const URI = 'file:///proj/app.ts';
const wholeFile = (text: string) => [{ range: { startLineNumber: 1, startColumn: 1, endLineNumber: 1_000_000, endColumn: 1 }, text }];
const v1 = 'a\nb\nc\nd\ne';
const v2 = 'a\nb'; // removes c, d, e (3 removed, 0 added)
const timeline: EditTimelineLike = {
fileBaselines: [[`${URI}::r1`, { uri: { external: URI }, requestId: 'r1', content: v1 }]],
operations: [
{ type: 'textEdit', requestId: 'r1', uri: { external: URI }, epoch: 1, edits: wholeFile(v1) }, // equals baseline -> 0/0
{ type: 'textEdit', requestId: 'r1', uri: { external: URI }, epoch: 2, edits: wholeFile(v2) }, // 0 added, 3 removed
],
};
const editLocIndex = new Map<string, Map<string, { added: number; removed: number }>>();
accumulateEditLoc(timeline, editLocIndex);

const ts = new Date(2024, 5, 15, 10, 0, 0).getTime();
const day = '2024-06-15';
const sessions = [
makeSession({
sessionId: 's1', harness: 'Codex', creationDate: ts, workspaceName: 'proj',
requests: [makeRequest({ requestId: 'r1', timestamp: ts, modelId: 'gpt-5', aiCode: [{ language: 'typescript', loc: 5 }] })],
}),
];
const prod = new Analyzer(sessions, editLocIndex).getCodeProduction();
// Gross = 5 code-block LoC + 0 edit-added; removed = 3; net = 5 - 3 = 2.
expect(prod.summary.totalAiLoc).toBe(5);
expect(prod.summary.totalRemovedAiLoc).toBe(3);
expect(prod.summary.totalNetAiLoc).toBe(2);

const dayIdx = prod.dailyTimeline.labels.indexOf(day);
expect(dayIdx).toBeGreaterThanOrEqual(0);
expect(prod.dailyTimeline.aiLoc[dayIdx]).toBe(5);
expect(prod.dailyTimeline.removedLoc[dayIdx]).toBe(3);
// Removed is broken down by dimension and aligns with the same day index.
expect(prod.dailyRemovedByModel['gpt-5'][dayIdx]).toBe(3);
expect(prod.dailyRemovedByWorkspace['proj'][dayIdx]).toBe(3);
expect(prod.dailyRemovedByHarness['Codex'][dayIdx]).toBe(3);
});
});

7 changes: 4 additions & 3 deletions src/core/analyzer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import { ContextAnalyzer } from './analyzer-context';
import { InsightsAnalyzer } from './analyzer-insights';
import { ImageAnalyzer, ImageGalleryData } from './analyzer-images';
import { AnalyzerBase } from './analyzer-base';
import { EditLoc, EditLocIndex } from './edit-loc-diff';
import { errorCore, infoCore, warnCore } from './log';

export class Analyzer {
Expand All @@ -42,12 +43,12 @@ export class Analyzer {
private readonly context: ContextAnalyzer;
private readonly images: ImageAnalyzer;
private readonly sessions: Session[];
private readonly editLocIndex: Map<string, Map<string, number>>;
private readonly editLocIndex: EditLocIndex;
private readonly workspaces: Map<string, Workspace>;
private cache = new Map<string, unknown>();

constructor(sessions: Session[], editLocIndex?: Map<string, Map<string, number>>, workspaces?: Map<string, Workspace>) {
const elIdx = editLocIndex ?? new Map<string, Map<string, number>>();
constructor(sessions: Session[], editLocIndex?: EditLocIndex, workspaces?: Map<string, Workspace>) {
const elIdx = editLocIndex ?? new Map<string, Map<string, EditLoc>>();
this.sessions = sessions;
this.editLocIndex = elIdx;
this.workspaces = workspaces ?? new Map<string, Workspace>();
Expand Down
Loading
Loading