feat: add frontend TypeScript types and IPC layer

Establishes type-safe communication between React frontend and Rust backend.
Types mirror the Rust structs to ensure consistency across the Tauri boundary.

Type definitions (src/lib/types.ts):
- LoreStatus, LoreSummaryStatus: Lore CLI response shapes
- BridgeStatus, SyncResult: Bridge operation results
- McError, McErrorCode: Structured error handling with type guard
- FocusItem, FocusItemType: THE ONE THING work items
- FocusAction, DecisionEntry: User action tracking
- Staleness computation: fresh/normal/amber/urgent based on age

IPC wrapper (src/lib/tauri.ts):
- Typed invoke() calls for each Rust command
- getLoreStatus, getBridgeStatus, syncNow, reconcile, quickCapture

Data transformation (src/lib/transform.ts):
- transformLoreData: Converts lore response to FocusItem[]
- Priority ordering: reviews first (blocking others), issues, authored MRs
- Generates stable IDs matching bridge mapping keys

Formatting utilities (src/lib/format.ts):
- formatIid: Prefix with ! for MRs, # for issues

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
teernisse
2026-02-26 09:54:15 -05:00
parent 2e0ead8660
commit e01e93f846
4 changed files with 265 additions and 0 deletions

132
src/lib/types.ts Normal file
View File

@@ -0,0 +1,132 @@
/**
* TypeScript types mirroring the Rust backend data structures.
*
* These are used by the IPC layer and components to maintain
* type safety across the Tauri boundary.
*/
// -- Backend response types (match Rust structs in commands/mod.rs) --
export interface LoreStatus {
last_sync: string | null;
is_healthy: boolean;
message: string;
summary: LoreSummaryStatus | null;
}
export interface LoreSummaryStatus {
open_issues: number;
authored_mrs: number;
reviewing_mrs: number;
}
export interface BridgeStatus {
mapping_count: number;
pending_count: number;
suspect_count: number;
last_sync: string | null;
last_reconciliation: string | null;
}
export interface SyncResult {
created: number;
closed: number;
skipped: number;
/** Number of suspect_orphan flags cleared (item reappeared) */
healed: number;
/** Error messages from non-fatal errors during sync */
errors: string[];
}
// -- Structured error types (match Rust error.rs) --
/** Error codes for programmatic handling */
export type McErrorCode =
| "LORE_UNAVAILABLE"
| "LORE_UNHEALTHY"
| "LORE_FETCH_FAILED"
| "BRIDGE_LOCKED"
| "BRIDGE_MAP_CORRUPTED"
| "BRIDGE_SYNC_FAILED"
| "BEADS_UNAVAILABLE"
| "BEADS_CREATE_FAILED"
| "BEADS_CLOSE_FAILED"
| "IO_ERROR"
| "INTERNAL_ERROR";
/** Structured error from Tauri IPC commands */
export interface McError {
code: McErrorCode;
message: string;
recoverable: boolean;
}
/** Type guard to check if an error is a structured McError */
export function isMcError(err: unknown): err is McError {
return (
typeof err === "object" &&
err !== null &&
"code" in err &&
"message" in err &&
"recoverable" in err
);
}
/** Result from the quick_capture command */
export interface CaptureResult {
bead_id: string;
}
// -- Frontend-only types --
/** The type of work item surfaced in the Focus View */
export type FocusItemType = "mr_review" | "issue" | "mr_authored" | "manual";
/** A single work item that can be THE ONE THING */
export interface FocusItem {
/** Unique key matching bridge mapping (e.g., "mr_review:g/p:847") */
id: string;
/** Human-readable title */
title: string;
/** Type badge to display */
type: FocusItemType;
/** Project path (e.g., "platform/core") */
project: string;
/** URL to open in browser (GitLab link) */
url: string;
/** Entity IID (e.g., MR !847, Issue #42) */
iid: number;
/** ISO timestamp of last update */
updatedAt: string | null;
/** Optional context quote (e.g., reviewer comment) */
contextQuote: string | null;
/** Who is requesting attention */
requestedBy: string | null;
}
/** Action the user takes on a focused item */
export type FocusAction = "start" | "defer_1h" | "defer_tomorrow" | "skip";
/** An entry in the decision log */
export interface DecisionEntry {
timestamp: string;
action: FocusAction;
itemId: string;
reason: string | null;
}
/** Staleness level derived from item age */
export type Staleness = "fresh" | "normal" | "amber" | "urgent";
/** Compute staleness from an ISO timestamp */
export function computeStaleness(updatedAt: string | null): Staleness {
if (!updatedAt) return "normal";
const ageMs = Date.now() - new Date(updatedAt).getTime();
const ageDays = ageMs / (1000 * 60 * 60 * 24);
if (ageDays < 1) return "fresh";
if (ageDays < 3) return "normal";
if (ageDays < 7) return "amber";
return "urgent";
}