refactor: isolate operational metadata storage

This commit is contained in:
Brad Groux 2026-08-23 20:32:09 -05:00
parent 97ac04b968
commit bab205757a
5 changed files with 68 additions and 48 deletions

View file

@ -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",

View file

@ -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<AgentHealthStatus> {
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<ProviderVersionProbe> {
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,

View file

@ -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' };

View file

@ -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<VelocityMetrics> {
// 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<string, string>();
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
}

View file

@ -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<ExecutablePathProbe> {
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<string | undefined> {
const metadata = JSON.parse(await readFile(packageUrl, 'utf8')) as { version?: unknown };
return typeof metadata.version === 'string' ? metadata.version : undefined;
}
async readSprintLabels(runtimeDir: string): Promise<Map<string, string>> {
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();