stemedb/applications/aphoria-dashboard/src/lib/api/client.ts
jml cce54358d2 feat(aphoria): add git commit tracking + comprehensive documentation
**Git Commit Tracking**
- Automatically capture git commit hash when claims/observations are ingested
- Store in assertion metadata for temporal context and audit trails
- Graceful degradation in non-git environments
- Solves double-commit problem by capturing hash at ingestion time

**Implementation**
- walker/git.rs: get_current_commit_hash() utility function
- bridge.rs: Accept optional git_commit parameter in all conversion functions
- episteme/local: Store project_root, capture git hash during ingestion
- 5 new tests for git hash tracking + metadata validation
- All 1162 aphoria tests passing

**Documentation Overhaul**
- README: Added Observations vs Claims distinction, git tracking, dashboard
- CLI Reference: New sections for git integration and ignore/exclusion system
- Comprehensive ignore documentation: .aphoriaignore, inline comments, 4 methods
- Enhanced verification engine docs with matching capabilities
- DOCUMENTATION_UPDATES.md: Complete audit summary

**Dashboard Separation**
- Moved Aphoria-specific UI from stemedb-dashboard to aphoria-dashboard
- Clean separation of concerns: StemeDB for core, Aphoria for security
- Added dashboard documentation and setup guides

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-02-08 18:36:46 +00:00

277 lines
8.3 KiB
TypeScript

import {
ApiError,
type HealthResponse,
type SkepticResponse,
type LayeredResponse,
type QuarantineResponse,
type CircuitBreakerResponse,
type AuditResponse,
type ListSourcesResponse,
type SourceImpactResponse,
type QuarantineSourceResponse,
type RestoreSourceResponse,
type GetPatternsResponse,
type ScanRequest,
type ScanResponse,
type ListScansResponse,
type ListClaimsRequest,
type ListClaimsResponse,
type CreateClaimRequest,
type CreateClaimResponse,
type UpdateClaimRequest,
type UpdateClaimResponse,
type DeprecateClaimRequest,
type DeprecateClaimResponse,
type VerifyClaimsRequest,
type VerifyReportResponse,
type CoverageRequest,
type CoverageReportResponse,
type AcknowledgeViolationRequest,
type AcknowledgeViolationResponse,
} from "./types";
export class StemeDBClient {
private baseUrl: string;
private apiKey: string | null;
constructor(baseUrl?: string, apiKey?: string) {
// Support empty string for relative URLs (proxied setup)
// If NEXT_PUBLIC_STEMEDB_API_URL is set to empty string, use ""
// Otherwise default to localhost for local dev
const envUrl = process.env.NEXT_PUBLIC_STEMEDB_API_URL;
this.baseUrl =
baseUrl !== undefined
? baseUrl
: envUrl !== undefined
? envUrl
: "http://127.0.0.1:18180";
this.apiKey = apiKey || process.env.STEMEDB_API_KEY || null;
}
private async fetch<T>(path: string, options?: RequestInit): Promise<T> {
const headers: HeadersInit = { "Content-Type": "application/json" };
if (this.apiKey) {
headers["X-API-Key"] = this.apiKey;
}
const response = await fetch(`${this.baseUrl}${path}`, {
...options,
headers: { ...headers, ...options?.headers },
cache: "no-store",
});
if (!response.ok) {
throw new ApiError(response.status, await response.text());
}
return response.json();
}
async health(): Promise<HealthResponse> {
return this.fetch<HealthResponse>("/health");
}
async skeptic(
subject: string,
predicate: string,
includeSourceMetadata = true,
asOf?: number
): Promise<SkepticResponse> {
const params = new URLSearchParams({
subject,
predicate,
include_source_metadata: String(includeSourceMetadata),
});
if (asOf !== undefined) {
params.set("as_of", String(asOf));
}
return this.fetch<SkepticResponse>(`/v1/skeptic?${params}`);
}
async layered(
subject: string,
predicate: string,
asOf?: number
): Promise<LayeredResponse> {
const params = new URLSearchParams({ subject, predicate });
if (asOf !== undefined) {
params.set("as_of", String(asOf));
}
return this.fetch<LayeredResponse>(`/v1/layered?${params}`);
}
async quarantine(limit = 50, offset = 0): Promise<QuarantineResponse> {
const params = new URLSearchParams({
limit: String(limit),
});
// Note: offset not yet supported by backend
void offset;
return this.fetch<QuarantineResponse>(`/v1/admin/quarantine?${params}`);
}
async restoreFromQuarantine(hash: string): Promise<void> {
await this.fetch(`/v1/admin/quarantine/${hash}/approve`, { method: "POST" });
}
async deleteFromQuarantine(hash: string): Promise<void> {
await this.fetch(`/v1/admin/quarantine/${hash}/reject`, { method: "POST" });
}
async circuitBreakers(): Promise<CircuitBreakerResponse> {
return this.fetch<CircuitBreakerResponse>("/v1/admin/circuit-breakers/tripped");
}
async resetCircuitBreaker(agentId: string): Promise<void> {
await this.fetch(`/v1/admin/circuit-breaker/reset`, {
method: "POST",
body: JSON.stringify({ agent_id: agentId }),
});
}
async auditQueries(params: {
limit?: number;
agentId?: string;
subject?: string;
predicate?: string;
from?: number;
to?: number;
} = {}): Promise<AuditResponse> {
const searchParams = new URLSearchParams({ limit: String(params.limit ?? 100) });
if (params.agentId) searchParams.set("agent_id", params.agentId);
if (params.subject) searchParams.set("subject", params.subject);
if (params.predicate) searchParams.set("predicate", params.predicate);
if (params.from !== undefined) searchParams.set("from", String(params.from));
if (params.to !== undefined) searchParams.set("to", String(params.to));
return this.fetch<AuditResponse>(`/v1/audit/queries?${searchParams}`);
}
// Source Registry methods
async listSources(limit = 100): Promise<ListSourcesResponse> {
const params = new URLSearchParams({ limit: String(limit) });
return this.fetch<ListSourcesResponse>(`/v1/sources?${params}`);
}
async getSourceImpact(hash: string): Promise<SourceImpactResponse> {
return this.fetch<SourceImpactResponse>(`/v1/sources/${hash}/impact`);
}
async quarantineSource(
hash: string,
preview: boolean,
reason?: string
): Promise<QuarantineSourceResponse> {
return this.fetch<QuarantineSourceResponse>(
`/v1/sources/${hash}/quarantine`,
{
method: "POST",
body: JSON.stringify({ preview, reason }),
}
);
}
async restoreSource(
hash: string,
reason?: string
): Promise<RestoreSourceResponse> {
return this.fetch<RestoreSourceResponse>(`/v1/sources/${hash}/restore`, {
method: "POST",
body: JSON.stringify({ reason }),
});
}
getSourceImpactExportUrl(hash: string, format: "csv" | "json"): string {
return `${this.baseUrl}/v1/sources/${hash}/impact/export?format=${format}`;
}
getApiKey(): string | null {
return this.apiKey;
}
// Aphoria methods
async getPatterns(params: {
subjectPrefix?: string;
minProjects?: number;
limit?: number;
} = {}): Promise<GetPatternsResponse> {
const searchParams = new URLSearchParams();
if (params.subjectPrefix) searchParams.set("subject_prefix", params.subjectPrefix);
if (params.minProjects !== undefined) searchParams.set("min_projects", String(params.minProjects));
if (params.limit !== undefined) searchParams.set("limit", String(params.limit));
const query = searchParams.toString();
return this.fetch<GetPatternsResponse>(`/v1/aphoria/patterns${query ? `?${query}` : ""}`);
}
async runScan(request: ScanRequest): Promise<ScanResponse> {
return this.fetch<ScanResponse>("/v1/aphoria/scan", {
method: "POST",
body: JSON.stringify(request),
});
}
async listScans(): Promise<ListScansResponse> {
return this.fetch<ListScansResponse>("/v1/aphoria/scans");
}
// Claims Management methods
async listClaims(request: ListClaimsRequest): Promise<ListClaimsResponse> {
return this.fetch<ListClaimsResponse>("/v1/aphoria/claims/list", {
method: "POST",
body: JSON.stringify(request),
});
}
async createClaim(request: CreateClaimRequest): Promise<CreateClaimResponse> {
return this.fetch<CreateClaimResponse>("/v1/aphoria/claims/create", {
method: "POST",
body: JSON.stringify(request),
});
}
async updateClaim(request: UpdateClaimRequest): Promise<UpdateClaimResponse> {
return this.fetch<UpdateClaimResponse>("/v1/aphoria/claims/update", {
method: "POST",
body: JSON.stringify(request),
});
}
async deprecateClaim(request: DeprecateClaimRequest): Promise<DeprecateClaimResponse> {
return this.fetch<DeprecateClaimResponse>("/v1/aphoria/claims/deprecate", {
method: "POST",
body: JSON.stringify(request),
});
}
async verifyClaims(request: VerifyClaimsRequest): Promise<VerifyReportResponse> {
return this.fetch<VerifyReportResponse>("/v1/aphoria/claims/verify", {
method: "POST",
body: JSON.stringify(request),
});
}
async getCoverage(request: CoverageRequest): Promise<CoverageReportResponse> {
return this.fetch<CoverageReportResponse>("/v1/aphoria/claims/coverage", {
method: "POST",
body: JSON.stringify(request),
});
}
async acknowledgeViolation(
request: AcknowledgeViolationRequest
): Promise<AcknowledgeViolationResponse> {
return this.fetch<AcknowledgeViolationResponse>("/v1/aphoria/claims/acknowledge", {
method: "POST",
body: JSON.stringify(request),
});
}
}
// Singleton client for server components
let _client: StemeDBClient | null = null;
export function getClient(): StemeDBClient {
if (!_client) {
_client = new StemeDBClient();
}
return _client;
}