Skip to content
Merged
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
31 changes: 4 additions & 27 deletions src/oclif/commands/curate/view.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,33 +6,10 @@ import type {CurateLogStatus} from '../../../server/core/interfaces/storage/i-cu
import {FileCurateLogStore} from '../../../server/infra/storage/file-curate-log-store.js'
import {CurateLogUseCase} from '../../../server/infra/usecase/curate-log-use-case.js'
import {getProjectDataDir} from '../../../server/utils/path-utils.js'
import {parseTimeFilter} from '../../lib/time-filter.js'

const VALID_STATUSES: CurateLogStatus[] = ['cancelled', 'completed', 'error', 'processing']

const RELATIVE_TIME_PATTERN = /^(\d+)(m|h|d|w)$/

/**
* Parse a time filter value into a UTC millisecond timestamp.
*
* Accepts:
* - Relative: "30m", "1h", "24h", "7d", "2w"
* - Absolute: ISO date "2024-01-15" or datetime "2024-01-15T12:00:00Z"
*
* Returns null when the value cannot be parsed.
*/
function parseTimeFilter(value: string): null | number {
const relMatch = RELATIVE_TIME_PATTERN.exec(value)
if (relMatch) {
const amount = Number(relMatch[1])
const unit = relMatch[2]
const multipliers: Record<string, number> = {d: 86_400_000, h: 3_600_000, m: 60_000, w: 604_800_000}
return Date.now() - amount * multipliers[unit]
}

const ts = new Date(value).getTime()
return Number.isNaN(ts) ? null : ts
}

export default class CurateView extends Command {
static args = {
id: Args.string({
Expand All @@ -55,7 +32,7 @@ export default class CurateView extends Command {
]
static flags = {
before: Flags.string({
description: 'Show entries started before this time (ISO date or relative: 1h, 24h, 7d, 2w)',
description: 'Show entries started before this time (ISO date or relative: 30m, 1h, 24h, 7d, 2w)',
}),
detail: Flags.boolean({
default: false,
Expand All @@ -72,7 +49,7 @@ export default class CurateView extends Command {
min: 1,
}),
since: Flags.string({
description: 'Show entries started after this time (ISO date or relative: 1h, 24h, 7d, 2w)',
description: 'Show entries started after this time (ISO date or relative: 30m, 1h, 24h, 7d, 2w)',
}),
status: Flags.string({
description: `Filter by status (can be repeated). Options: ${VALID_STATUSES.join(', ')}`,
Expand Down Expand Up @@ -111,7 +88,7 @@ export default class CurateView extends Command {
const ts = parseTimeFilter(value)
if (ts === null) {
this.error(
`Invalid time value for ${flagName}: "${value}". Use ISO date (2024-01-15) or relative (1h, 24h, 7d, 2w).`,
`Invalid time value for ${flagName}: "${value}". Use ISO date (2024-01-15) or relative (30m, 1h, 24h, 7d, 2w).`,
{exit: 2},
)
}
Expand Down
107 changes: 107 additions & 0 deletions src/oclif/commands/query-log/view.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
import {findProjectRoot} from '@campfirein/brv-transport-client'
import {Args, Command, Flags} from '@oclif/core'

import {QUERY_LOG_STATUSES, QUERY_LOG_TIERS, type QueryLogStatus, type QueryLogTier} from '../../../server/core/domain/entities/query-log-entry.js'
import {FileQueryLogStore} from '../../../server/infra/storage/file-query-log-store.js'
import {QueryLogUseCase} from '../../../server/infra/usecase/query-log-use-case.js'
import {getProjectDataDir} from '../../../server/utils/path-utils.js'
import {parseTimeFilter} from '../../lib/time-filter.js'

export default class QueryLogView extends Command {
static args = {
id: Args.string({
description: 'Query log entry ID to view in detail',
required: false,
}),
}
static description = 'View query log history'
static examples = [
'<%= config.bin %> <%= command.id %>',
'<%= config.bin %> <%= command.id %> qry-1712345678901',
'<%= config.bin %> <%= command.id %> --limit 20',
'<%= config.bin %> <%= command.id %> --status completed',
'<%= config.bin %> <%= command.id %> --status completed --status error',
'<%= config.bin %> <%= command.id %> --tier 0 --tier 1',
'<%= config.bin %> <%= command.id %> --since 1h',
'<%= config.bin %> <%= command.id %> --since 2024-01-15',
'<%= config.bin %> <%= command.id %> --before 2024-02-01',
'<%= config.bin %> <%= command.id %> --detail',
'<%= config.bin %> <%= command.id %> --format json',
]
static flags = {
before: Flags.string({
description: 'Show entries started before this time (ISO date or relative: 30m, 1h, 24h, 7d, 2w)',
}),
detail: Flags.boolean({
default: false,
description: 'Show matched docs for each entry',
}),
format: Flags.string({
default: 'text',
description: 'Output format',
options: ['text', 'json'],
}),
limit: Flags.integer({
default: 10,
description: 'Maximum number of log entries to display',
min: 1,
}),
since: Flags.string({
description: 'Show entries started after this time (ISO date or relative: 30m, 1h, 24h, 7d, 2w)',
}),
status: Flags.string({
description: `Filter by status (can be repeated). Options: ${QUERY_LOG_STATUSES.join(', ')}`,
multiple: true,
options: QUERY_LOG_STATUSES,
}),
tier: Flags.string({
description: `Filter by resolution tier (can be repeated). Options: ${QUERY_LOG_TIERS.join(', ')}`,
multiple: true,
options: QUERY_LOG_TIERS.map(String),
}),
}

protected createDependencies(baseDir: string) {
const store = new FileQueryLogStore({baseDir})
const useCase = new QueryLogUseCase({
queryLogStore: store,
terminal: {log: (m) => this.log(m ?? '')},
})
return {useCase}
}

async run(): Promise<void> {
const {args, flags} = await this.parse(QueryLogView)

const after = flags.since ? this.parseTime(flags.since, '--since') : undefined
const before = flags.before ? this.parseTime(flags.before, '--before') : undefined
const format: 'json' | 'text' = flags.format === 'json' ? 'json' : 'text'

const projectRoot = (await findProjectRoot(process.cwd())) ?? process.cwd()
const baseDir = getProjectDataDir(projectRoot)
const {useCase} = this.createDependencies(baseDir)

await useCase.run({
after,
before,
detail: flags.detail,
format,
id: args.id,
limit: flags.limit,
status: flags.status?.filter((s): s is QueryLogStatus => (QUERY_LOG_STATUSES as readonly string[]).includes(s)),
tier: flags.tier?.map(Number).filter((t): t is QueryLogTier => (QUERY_LOG_TIERS as readonly number[]).includes(t)),
})
}

private parseTime(value: string, flagName: string): number {
const ts = parseTimeFilter(value)
if (ts === null) {
this.error(
`Invalid time value for ${flagName}: "${value}". Use ISO date (2024-01-15) or relative (30m, 1h, 24h, 7d, 2w).`,
{exit: 2},
)
}

return ts
}
}
23 changes: 23 additions & 0 deletions src/oclif/lib/time-filter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
const RELATIVE_TIME_PATTERN = /^(\d+)(m|h|d|w)$/

/**
* Parse a time filter value into a UTC millisecond timestamp.
*
* Accepts:
* - Relative: "30m", "1h", "24h", "7d", "2w"
* - Absolute: ISO date "2024-01-15" or datetime "2024-01-15T12:00:00Z"
*
* Returns null when the value cannot be parsed.
*/
export function parseTimeFilter(value: string): null | number {
const relMatch = RELATIVE_TIME_PATTERN.exec(value)
if (relMatch) {
const amount = Number(relMatch[1])
const unit = relMatch[2]
const multipliers: Record<string, number> = {d: 86_400_000, h: 3_600_000, m: 60_000, w: 604_800_000}
return Date.now() - amount * multipliers[unit]
}

const ts = new Date(value).getTime()
return Number.isNaN(ts) ? null : ts
}
14 changes: 14 additions & 0 deletions src/server/core/domain/entities/query-log-entry.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
// Stub: domain types driven by brv query-log view command (ENG-1897).
// Full QueryLogEntry discriminated union will be added in ENG-1887.

// ── Single source of truth: runtime arrays → derived types ───────────────────
// Tiers originate from QueryExecutor (src/server/infra/executor/query-executor.ts).
// Statuses track the entry lifecycle. Both are domain concepts.

/** Valid resolution tiers. Add/remove here — the type updates automatically. */
export const QUERY_LOG_TIERS = [0, 1, 2, 3, 4] as const
export type QueryLogTier = (typeof QUERY_LOG_TIERS)[number]

/** Valid query log statuses. Add/remove here — the type updates automatically. */
export const QUERY_LOG_STATUSES = ['cancelled', 'completed', 'error', 'processing'] as const
export type QueryLogStatus = (typeof QUERY_LOG_STATUSES)[number]
6 changes: 6 additions & 0 deletions src/server/core/interfaces/storage/i-query-log-store.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
// Stub: types driven by brv query-log view command (ENG-1897).
// Full IQueryLogStore interface will be added in ENG-1888.

// Re-export domain types — single source of truth is in the entity.
export {QUERY_LOG_STATUSES, QUERY_LOG_TIERS} from '../../domain/entities/query-log-entry.js'
export type {QueryLogStatus, QueryLogTier} from '../../domain/entities/query-log-entry.js'
17 changes: 17 additions & 0 deletions src/server/core/interfaces/usecase/i-query-log-use-case.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
// Interface driven by brv query-log view command (ENG-1897).
// Full implementation in ENG-1895.

import type {QueryLogStatus, QueryLogTier} from '../storage/i-query-log-store.js'

export interface IQueryLogUseCase {
run(options: {
after?: number
before?: number
detail?: boolean
format?: 'json' | 'text'
id?: string
limit?: number
status?: QueryLogStatus[]
tier?: QueryLogTier[]
}): Promise<void>
}
10 changes: 10 additions & 0 deletions src/server/infra/storage/file-query-log-store.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
// Stub: minimal FileQueryLogStore for compilation (ENG-1897).
// Full implementation with Zod validation, atomic writes, and pruning in ENG-1889.

export class FileQueryLogStore {
readonly baseDir: string

constructor(opts: {baseDir: string}) {
this.baseDir = opts.baseDir
}
}
19 changes: 19 additions & 0 deletions src/server/infra/usecase/query-log-use-case.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
// Stub: minimal QueryLogUseCase for compilation (ENG-1897).
// Full implementation with list/detail views in ENG-1896.

import type {IQueryLogUseCase} from '../../core/interfaces/usecase/i-query-log-use-case.js'

type Terminal = {log(msg?: string): void}

type QueryLogUseCaseDeps = {
queryLogStore: unknown
terminal: Terminal
}

export class QueryLogUseCase implements IQueryLogUseCase {
constructor(private readonly deps: QueryLogUseCaseDeps) {}

async run(_options: Parameters<IQueryLogUseCase['run']>[0]): Promise<void> {
// Stub: real implementation in ENG-1896
}
}
Loading
Loading