stemedb/applications/stemedb-dashboard/src/components/claims/claims-panel.tsx
jml ef2c8c5940 fix(aphoria): fix 3 critical verification engine bugs
Fixed 3 bugs in Aphoria's claim verification engine that were causing
false positives in Maxwell validation testing:

**Bug 1: Path matching + predicate filtering**
- Added predicate filtering to prevent cross-predicate matches
- Added path prefix matching to respect crate boundaries
- Prevents core/imports/serde from matching hypervisor/vsock/imports/serde

**Bug 2: Value-specific absent checks**
- Absent mode now checks for specific forbidden value, not any observation
- Example: "Clone absent" + "Debug present" = PASS (not CONFLICT)
- Only conflicts when the exact forbidden value is found

**Bug 3: Wildcard pattern support**
- Wildcard patterns like message/*/derives now match multiple paths
- Enhanced wildcard_matches() to support prefix/*/suffix patterns
- Correctly strips full scheme+language from observation paths

**Test coverage:**
- All 39 existing tests passing
- 3 new tests added for bug fixes
- 2 tests updated to use correct predicates
- Zero clippy warnings

**Maxwell validation:**
- maxwell-core-no-serde-001: CONFLICT → PASS (respects path boundaries)
- maxwell-singleton-no-clone-001: CONFLICT → PASS (value-specific absent)
- 5 claims now correctly show as MISSING (expose predicate mismatches)

The fixes successfully eliminate false positives while exposing pre-existing
issues where claims used incorrect predicates.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-02-08 15:13:10 +00:00

275 lines
10 KiB
TypeScript

"use client";
import { useState } from "react";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { Input } from "@/components/ui/input";
import { Button } from "@/components/ui/button";
import { getClient } from "@/lib/api/client";
import type {
AuthoredClaimDto,
ListClaimsResponse,
VerifyReportResponse,
CoverageReportResponse,
} from "@/lib/api/types";
import { ClaimsLoadingSkeleton } from "./claims-loading-skeleton";
import { ClaimsEmptyState } from "./claims-empty-state";
type PanelState<T> =
| { status: "idle" }
| { status: "loading" }
| { status: "success"; data: T }
| { status: "error"; error: string };
function getDefaultProjectPath(): string {
return process.env.NEXT_PUBLIC_DEFAULT_PROJECT_PATH || "/home/jml/Workspace/stemedb";
}
export function ClaimsPanel() {
const [projectPath, setProjectPath] = useState(getDefaultProjectPath());
const [claimsState, setClaimsState] = useState<PanelState<ListClaimsResponse>>({
status: "idle",
});
const [verifyState, setVerifyState] = useState<PanelState<VerifyReportResponse>>({
status: "idle",
});
const [coverageState, setCoverageState] = useState<PanelState<CoverageReportResponse>>({
status: "idle",
});
const [selectedClaim, setSelectedClaim] = useState<AuthoredClaimDto | null>(null);
const client = getClient();
const loadClaims = async () => {
setClaimsState({ status: "loading" });
try {
const data = await client.listClaims({ project_path: projectPath });
setClaimsState({ status: "success", data });
} catch (error) {
setClaimsState({
status: "error",
error: error instanceof Error ? error.message : "Failed to load claims",
});
}
};
const runVerification = async () => {
setVerifyState({ status: "loading" });
try {
const data = await client.verifyClaims({ project_path: projectPath });
setVerifyState({ status: "success", data });
} catch (error) {
setVerifyState({
status: "error",
error: error instanceof Error ? error.message : "Verification failed",
});
}
};
const loadCoverage = async () => {
setCoverageState({ status: "loading" });
try {
const data = await client.getCoverage({ project_path: projectPath });
setCoverageState({ status: "success", data });
} catch (error) {
setCoverageState({
status: "error",
error: error instanceof Error ? error.message : "Coverage load failed",
});
}
};
return (
<div className="space-y-6">
{/* Project Path Input */}
<Card>
<CardHeader>
<CardTitle>Project Configuration</CardTitle>
<CardDescription>
Select the project to analyze claims for
</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="space-y-2">
<label htmlFor="project-path" className="text-sm font-medium">
Project Path
</label>
<div className="flex gap-2">
<Input
id="project-path"
value={projectPath}
onChange={(e) => setProjectPath(e.target.value)}
placeholder="/path/to/project"
/>
<Button onClick={loadClaims}>Load Claims</Button>
</div>
</div>
</CardContent>
</Card>
{/* Tabs for Claims / Verify / Coverage */}
<Tabs defaultValue="claims" className="w-full">
<TabsList className="grid w-full grid-cols-3">
<TabsTrigger value="claims">Claims List</TabsTrigger>
<TabsTrigger value="verify">Verification</TabsTrigger>
<TabsTrigger value="coverage">Coverage</TabsTrigger>
</TabsList>
{/* Claims List Tab */}
<TabsContent value="claims">
{claimsState.status === "loading" && <ClaimsLoadingSkeleton />}
{claimsState.status === "error" && (
<Card>
<CardContent className="pt-6">
<p className="text-destructive">{claimsState.error}</p>
</CardContent>
</Card>
)}
{claimsState.status === "success" &&
(claimsState.data.claims.length === 0 ? (
<ClaimsEmptyState />
) : (
<Card>
<CardHeader>
<CardTitle>
Authored Claims ({claimsState.data.claims.length})
</CardTitle>
</CardHeader>
<CardContent>
<div className="space-y-2">
{claimsState.data.claims.map((claim) => (
<div
key={claim.id}
className="border rounded-lg p-4 hover:bg-muted/50 cursor-pointer"
onClick={() => setSelectedClaim(claim)}
>
<div className="flex items-center justify-between">
<div className="space-y-1">
<p className="font-mono text-sm">{claim.id}</p>
<p className="text-sm text-muted-foreground">
{claim.concept_path}
</p>
</div>
<div className="text-sm text-muted-foreground">
{claim.category}
</div>
</div>
</div>
))}
</div>
</CardContent>
</Card>
))}
{claimsState.status === "idle" && (
<Card>
<CardContent className="pt-6">
<p className="text-muted-foreground text-center">
Enter a project path and click "Load Claims" to begin
</p>
</CardContent>
</Card>
)}
</TabsContent>
{/* Verification Tab */}
<TabsContent value="verify">
<Card>
<CardHeader>
<CardTitle>Claim Verification</CardTitle>
<CardDescription>
Verify claims against extracted observations
</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<Button onClick={runVerification} disabled={verifyState.status === "loading"}>
{verifyState.status === "loading" ? "Running..." : "Run Verification"}
</Button>
{verifyState.status === "success" && (
<div className="space-y-4">
<div className="grid grid-cols-5 gap-4">
<div className="text-center">
<div className="text-2xl font-bold">{verifyState.data.summary.total_claims}</div>
<div className="text-sm text-muted-foreground">Total</div>
</div>
<div className="text-center">
<div className="text-2xl font-bold text-green-600">
{verifyState.data.summary.pass}
</div>
<div className="text-sm text-muted-foreground">Pass</div>
</div>
<div className="text-center">
<div className="text-2xl font-bold text-destructive">
{verifyState.data.summary.conflict}
</div>
<div className="text-sm text-muted-foreground">Conflict</div>
</div>
<div className="text-center">
<div className="text-2xl font-bold">{verifyState.data.summary.missing}</div>
<div className="text-sm text-muted-foreground">Missing</div>
</div>
<div className="text-center">
<div className="text-2xl font-bold">{verifyState.data.summary.unclaimed}</div>
<div className="text-sm text-muted-foreground">Unclaimed</div>
</div>
</div>
</div>
)}
{verifyState.status === "error" && (
<p className="text-destructive">{verifyState.error}</p>
)}
</CardContent>
</Card>
</TabsContent>
{/* Coverage Tab */}
<TabsContent value="coverage">
<Card>
<CardHeader>
<CardTitle>Coverage Metrics</CardTitle>
<CardDescription>
Per-module claim coverage analysis
</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<Button onClick={loadCoverage} disabled={coverageState.status === "loading"}>
{coverageState.status === "loading" ? "Computing..." : "Compute Coverage"}
</Button>
{coverageState.status === "success" && (
<div className="space-y-4">
<div className="grid grid-cols-3 gap-4">
<div className="text-center">
<div className="text-2xl font-bold">
{coverageState.data.summary.total_observations}
</div>
<div className="text-sm text-muted-foreground">Observations</div>
</div>
<div className="text-center">
<div className="text-2xl font-bold">
{coverageState.data.summary.total_claims}
</div>
<div className="text-sm text-muted-foreground">Claims</div>
</div>
<div className="text-center">
<div className="text-2xl font-bold">
{coverageState.data.summary.claimed_percentage.toFixed(1)}%
</div>
<div className="text-sm text-muted-foreground">Coverage</div>
</div>
</div>
</div>
)}
{coverageState.status === "error" && (
<p className="text-destructive">{coverageState.error}</p>
)}
</CardContent>
</Card>
</TabsContent>
</Tabs>
</div>
);
}