refactor(usage): share attribution and scope filtering

This commit is contained in:
Neil
2026-09-18 13:51:17 -07:00
parent 002ff3ddb8
commit b9f06230ad
5 changed files with 84 additions and 82 deletions
@@ -1,67 +1,13 @@
import { normalizeComparablePath } from '../usage/usage-path-comparison'
import { attributeUsageEvent } from '../usage/usage-event-attribution'
import type { UsageScanWorktreeRef } from '../usage/usage-provider-contract'
import type { UsageWorktreeResolver } from '../usage/usage-worktree-resolver'
import type { CodexUsageAttributedEvent, CodexUsageParsedEvent } from './types'
export type CodexUsageWorktreeRef = UsageScanWorktreeRef
function getDefaultProjectLabel(cwd: string | null): string {
if (!cwd) {
return 'Unknown location'
}
const parts = cwd.replace(/\\/g, '/').split('/').filter(Boolean)
if (parts.length >= 2) {
return parts.slice(-2).join('/')
}
return parts.at(-1) ?? cwd
}
function localDayFromTimestamp(timestamp: string): string | null {
const parsed = new Date(timestamp)
if (Number.isNaN(parsed.getTime())) {
return null
}
const year = parsed.getFullYear()
const month = String(parsed.getMonth() + 1).padStart(2, '0')
const day = String(parsed.getDate()).padStart(2, '0')
return `${year}-${month}-${day}`
}
export async function attributeCodexUsageEvent(
event: CodexUsageParsedEvent,
resolveWorktree: UsageWorktreeResolver
): Promise<CodexUsageAttributedEvent | null> {
const day = localDayFromTimestamp(event.timestamp)
if (!day) {
return null
}
let repoId: string | null = null
let worktreeId: string | null = null
let projectKey = 'unscoped'
let projectLabel = getDefaultProjectLabel(event.cwd)
if (event.cwd) {
const worktree = resolveWorktree(event.cwd)
if (worktree) {
repoId = worktree.repoId
worktreeId = worktree.worktreeId
projectKey = `worktree:${worktree.worktreeId}`
projectLabel = worktree.displayName
} else {
// Why: all-local mode should still collapse repeated off-Orca sessions by
// location, but those keys must normalize slash/case differences so the
// same folder does not fragment into multiple "projects" across platforms.
projectKey = `cwd:${normalizeComparablePath(event.cwd)}`
}
}
return {
...event,
day,
projectKey,
projectLabel,
repoId,
worktreeId
}
return attributeUsageEvent(event, resolveWorktree)
}
@@ -1,6 +1,6 @@
import type { CodexUsageRange, CodexUsageScope } from '../../shared/codex-usage-types'
import type { CodexUsagePersistedState } from './types'
import { getLocalUsageDay, getUsageRangeCutoff } from '../usage/usage-calendar-range'
import { filterUsageDaily, filterUsageSessions } from '../usage/usage-scope-filters'
export type ScopedCodexUsageModelRow = {
modelKey: string
@@ -19,16 +19,7 @@ export function getFilteredDaily(
scope: CodexUsageScope,
range: CodexUsageRange
) {
const cutoff = getUsageRangeCutoff(range)
return state.dailyAggregates.filter((entry) => {
if (cutoff && entry.day < cutoff) {
return false
}
if (scope === 'orca' && entry.worktreeId === null) {
return false
}
return true
})
return filterUsageDaily(state.dailyAggregates, scope, range)
}
export function getFilteredSessions(
@@ -36,20 +27,7 @@ export function getFilteredSessions(
scope: CodexUsageScope,
range: CodexUsageRange
) {
const cutoff = getUsageRangeCutoff(range)
return state.sessions.filter((session) => {
const day = getLocalUsageDay(session.lastTimestamp)
if (!day) {
return false
}
if (cutoff && day < cutoff) {
return false
}
if (scope === 'orca') {
return session.locationBreakdown.some((entry) => entry.worktreeId !== null)
}
return true
})
return filterUsageSessions(state.sessions, scope, range)
}
export function getScopedSessionModels(
+47
View File
@@ -0,0 +1,47 @@
import { getLocalUsageDay } from './usage-calendar-range'
import { normalizeComparablePath } from './usage-path-comparison'
import type { UsageWorktreeResolver } from './usage-worktree-resolver'
export type UnattributedUsageEvent = {
timestamp: string
cwd: string | null
}
export type UsageEventAttribution = {
day: string
projectKey: string
projectLabel: string
repoId: string | null
worktreeId: string | null
}
function defaultProjectLabel(cwd: string | null): string {
if (!cwd) {
return 'Unknown location'
}
const parts = cwd.replace(/\\/g, '/').split('/').filter(Boolean)
return parts.length >= 2 ? parts.slice(-2).join('/') : (parts.at(-1) ?? cwd)
}
export function attributeUsageEvent<T extends UnattributedUsageEvent>(
event: T,
resolveWorktree: UsageWorktreeResolver
): (T & UsageEventAttribution) | null {
const day = getLocalUsageDay(event.timestamp)
if (!day) {
return null
}
const worktree = event.cwd ? resolveWorktree(event.cwd) : null
return {
...event,
day,
projectKey: worktree
? `worktree:${worktree.worktreeId}`
: event.cwd
? `cwd:${normalizeComparablePath(event.cwd)}`
: 'unscoped',
projectLabel: worktree?.displayName ?? defaultProjectLabel(event.cwd),
repoId: worktree?.repoId ?? null,
worktreeId: worktree?.worktreeId ?? null
}
}
+1 -1
View File
@@ -7,7 +7,7 @@
* consumer, so only the scan envelope is shared.
*/
export type UsageProviderId = 'claude' | 'codex' | 'opencode' | `plugin:${string}`
export type UsageProviderId = 'claude' | 'codex' | 'devin' | 'opencode' | `plugin:${string}`
/** Scan input. Distinct from `UsageWorktreeRef` in usage-worktree-metadata, which lacks `repoId`. */
export type UsageScanWorktreeRef = {
+31
View File
@@ -0,0 +1,31 @@
import { getLocalUsageDay, getUsageRangeCutoff } from './usage-calendar-range'
export function filterUsageDaily<T extends { day: string; worktreeId: string | null }>(
daily: readonly T[],
scope: 'orca' | 'all',
range: '7d' | '30d' | '90d' | 'all'
): T[] {
const cutoff = getUsageRangeCutoff(range)
return daily.filter((entry) => {
if (cutoff && entry.day < cutoff) {
return false
}
return scope === 'all' || entry.worktreeId !== null
})
}
export function filterUsageSessions<
T extends {
lastTimestamp: string
locationBreakdown: readonly { worktreeId: string | null }[]
}
>(sessions: readonly T[], scope: 'orca' | 'all', range: '7d' | '30d' | '90d' | 'all'): T[] {
const cutoff = getUsageRangeCutoff(range)
return sessions.filter((session) => {
const day = getLocalUsageDay(session.lastTimestamp)
if (!day || (cutoff && day < cutoff)) {
return false
}
return scope === 'all' || session.locationBreakdown.some((entry) => entry.worktreeId !== null)
})
}