diff --git a/docs/architecture/service-filesystem-boundary.json b/docs/architecture/service-filesystem-boundary.json index cf5918d4..bb63c112 100644 --- a/docs/architecture/service-filesystem-boundary.json +++ b/docs/architecture/service-filesystem-boundary.json @@ -1,13 +1,7 @@ { "schemaVersion": 1, - "maximumEntries": 34, + "maximumEntries": 31, "entries": [ - { - "path": "server/src/services/agent-health-service.ts", - "category": "authoritative-persistence", - "owner": "#1187", - "rationale": "Operational evidence storage migration is tracked in issue #1187." - }, { "path": "server/src/services/attachment-service.ts", "category": "compatibility-debt", @@ -26,12 +20,6 @@ "owner": "#1189", "rationale": "Remaining process I/O migration is tracked in issue #1189." }, - { - "path": "server/src/services/codex-health-service.ts", - "category": "authoritative-persistence", - "owner": "#1187", - "rationale": "Operational evidence storage migration is tracked in issue #1187." - }, { "path": "server/src/services/config-service.ts", "category": "compatibility-debt", @@ -92,12 +80,6 @@ "owner": "#1188", "rationale": "Managed-content storage migration is tracked in issue #1188." }, - { - "path": "server/src/services/metrics/task-metrics.ts", - "category": "authoritative-persistence", - "owner": "#1187", - "rationale": "Operational evidence storage migration is tracked in issue #1187." - }, { "path": "server/src/services/metrics/telemetry-reader.ts", "category": "authoritative-persistence", diff --git a/server/src/services/agent-health-service.ts b/server/src/services/agent-health-service.ts index b5d22b71..bf07dfc5 100644 --- a/server/src/services/agent-health-service.ts +++ b/server/src/services/agent-health-service.ts @@ -1,10 +1,11 @@ -import fs from 'fs/promises'; -import { constants as fsConstants } from 'fs'; -import path from 'path'; import { execFile } from 'child_process'; import { promisify } from 'util'; import type { AgentConfig } from '@veritas-kanban/shared'; import { hasClaudeCodeBareAuthentication } from './claude-code-adapter.js'; +import { + OperationalMetadataRepository, + operationalMetadataRepository, +} from '../storage/operational-metadata-repository.js'; const execFileAsync = promisify(execFile); const PROVIDER_VERSION_TIMEOUT_MS = 5_000; @@ -64,7 +65,10 @@ export interface AgentHealthChecker { } export class AgentHealthService implements AgentHealthChecker { - constructor(private readonly runCommand: AgentHealthCommandRunner = defaultCommandRunner) {} + constructor( + private readonly runCommand: AgentHealthCommandRunner = defaultCommandRunner, + private readonly operationalMetadata: OperationalMetadataRepository = operationalMetadataRepository + ) {} async checkAgent(agent: AgentConfig): Promise { const checkedAt = new Date().toISOString(); @@ -98,14 +102,8 @@ export class AgentHealthService implements AgentHealthChecker { private async findExecutable(command: string): Promise<{ found: boolean; path?: string }> { if (!command.trim()) return { found: false }; - if (command.includes(path.sep)) { - try { - await fs.access(command, fsConstants.X_OK); - return { found: true, path: command }; - } catch { - return { found: false }; - } - } + const directPath = await this.operationalMetadata.probeExecutablePath(command); + if (directPath.directPath) return { found: directPath.found, path: directPath.path }; try { const { stdout } = await this.runCommand('which', [command]); @@ -119,7 +117,7 @@ export class AgentHealthService implements AgentHealthChecker { agent: AgentConfig, versionProbe: ProviderVersionProbe ): Promise<{ authenticated: boolean | null; error?: string; diagnostics?: string[] }> { - const command = path.basename(agent.command); + const command = this.operationalMetadata.basename(agent.command); const provider = agent.provider ?? ''; if (provider === 'codex-cloud' || command === 'gh') { @@ -264,7 +262,7 @@ export class AgentHealthService implements AgentHealthChecker { command: string, args: string[] ): Promise { - const source = `${path.basename(command)} ${args.join(' ')}`; + const source = `${this.operationalMetadata.basename(command)} ${args.join(' ')}`; try { const { stdout, stderr } = await this.runCommand(command, args, { timeout: PROVIDER_VERSION_TIMEOUT_MS, diff --git a/server/src/services/codex-health-service.ts b/server/src/services/codex-health-service.ts index 5afe4dc8..810bfdb8 100644 --- a/server/src/services/codex-health-service.ts +++ b/server/src/services/codex-health-service.ts @@ -1,7 +1,10 @@ import { execFile } from 'child_process'; -import { readFile } from 'fs/promises'; import { promisify } from 'util'; import { ConfigService } from './config-service.js'; +import { + OperationalMetadataRepository, + operationalMetadataRepository, +} from '../storage/operational-metadata-repository.js'; const execFileAsync = promisify(execFile); @@ -38,7 +41,9 @@ export interface CodexHealthStatus { export class CodexHealthService { private configService: ConfigService; - constructor() { + constructor( + private readonly operationalMetadata: OperationalMetadataRepository = operationalMetadataRepository + ) { this.configService = new ConfigService(); } @@ -111,12 +116,9 @@ export class CodexHealthService { await import('@openai/codex-sdk'); const moduleUrl = import.meta.resolve('@openai/codex-sdk'); const packageUrl = new URL('../package.json', moduleUrl); - const packageMetadata = JSON.parse(await readFile(packageUrl, 'utf8')) as { - version?: unknown; - }; return { available: true, - version: typeof packageMetadata.version === 'string' ? packageMetadata.version : undefined, + version: await this.operationalMetadata.readPackageVersion(packageUrl), }; } catch (error: any) { return { available: false, error: error.message || 'Codex SDK is not available' }; diff --git a/server/src/services/metrics/task-metrics.ts b/server/src/services/metrics/task-metrics.ts index 44b2aab2..1a3f7318 100644 --- a/server/src/services/metrics/task-metrics.ts +++ b/server/src/services/metrics/task-metrics.ts @@ -1,11 +1,13 @@ /** * Task-related metrics: task counts by status and sprint velocity. */ -import fs from 'fs/promises'; -import path from 'path'; import type { BlockedCategory } from '@veritas-kanban/shared'; import { TaskService } from '../task-service.js'; import { getRuntimeDir } from '../../utils/paths.js'; +import { + OperationalMetadataRepository, + operationalMetadataRepository, +} from '../../storage/operational-metadata-repository.js'; import type { TaskMetrics, VelocityTrend, @@ -101,7 +103,8 @@ export async function computeTaskMetrics( export async function computeVelocityMetrics( taskService: TaskService, project?: string, - limit = 10 + limit = 10, + operationalMetadata: OperationalMetadataRepository = operationalMetadataRepository ): Promise { // Get all tasks (active + archived) to calculate velocity const [activeTasks, archivedTasks] = await Promise.all([ @@ -112,12 +115,8 @@ export async function computeVelocityMetrics( // Load sprint labels from sprints.json for display const sprintLabels = new Map(); try { - const sprintsFile = path.join(getRuntimeDir(), 'sprints.json'); - const sprintsData = await fs.readFile(sprintsFile, 'utf-8'); - const sprints = JSON.parse(sprintsData) as Array<{ id: string; label: string }>; - for (const s of sprints) { - sprintLabels.set(s.id, s.label); - } + const persistedLabels = await operationalMetadata.readSprintLabels(getRuntimeDir()); + for (const [id, label] of persistedLabels) sprintLabels.set(id, label); } catch { // No sprints file or can't read it - will use IDs as labels } diff --git a/server/src/storage/operational-metadata-repository.ts b/server/src/storage/operational-metadata-repository.ts new file mode 100644 index 00000000..b9a78581 --- /dev/null +++ b/server/src/storage/operational-metadata-repository.ts @@ -0,0 +1,39 @@ +import { constants } from 'node:fs'; +import { access, readFile } from 'node:fs/promises'; +import path from 'node:path'; + +export interface ExecutablePathProbe { + directPath: boolean; + found: boolean; + path?: string; +} + +export class OperationalMetadataRepository { + async probeExecutablePath(command: string): Promise { + if (!command.includes(path.sep)) return { directPath: false, found: false }; + + try { + await access(command, constants.X_OK); + return { directPath: true, found: true, path: command }; + } catch { + return { directPath: true, found: false }; + } + } + + basename(filePath: string): string { + return path.basename(filePath); + } + + async readPackageVersion(packageUrl: URL): Promise { + const metadata = JSON.parse(await readFile(packageUrl, 'utf8')) as { version?: unknown }; + return typeof metadata.version === 'string' ? metadata.version : undefined; + } + + async readSprintLabels(runtimeDir: string): Promise> { + const content = await readFile(path.join(runtimeDir, 'sprints.json'), 'utf8'); + const sprints = JSON.parse(content) as Array<{ id: string; label: string }>; + return new Map(sprints.map((sprint) => [sprint.id, sprint.label])); + } +} + +export const operationalMetadataRepository = new OperationalMetadataRepository();