mirror of
https://github.com/BradGroux/veritas-kanban.git
synced 2026-08-28 02:44:59 +00:00
refactor: isolate operational metadata storage
This commit is contained in:
parent
97ac04b968
commit
bab205757a
5 changed files with 68 additions and 48 deletions
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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' };
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
39
server/src/storage/operational-metadata-repository.ts
Normal file
39
server/src/storage/operational-metadata-repository.ts
Normal 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();
|
||||
Loading…
Add table
Reference in a new issue