**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>
277 lines
8.3 KiB
TypeScript
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;
|
|
}
|