step 0.1 complete: Type-safe contracts for all backend communication and the batch/storage subsystem

This commit is contained in:
2026-03-04 10:16:05 +01:00
parent 0fcfa3e5bb
commit 8268881f41
11 changed files with 353 additions and 11 deletions

View File

@@ -11,7 +11,7 @@
## Phase 0 — API Contracts & Types
### Step 0.1 — Define backend API contract types
- [ ] Create `src/shared/api-types.ts` with all interfaces the Electron app needs to communicate with the backend:
- [x] Create `src/shared/api-types.ts` with all interfaces the Electron app needs to communicate with the backend:
- `ExecutionPlan`, `PlanStep`, `PlanAction` (action types: `create_record`, `update_record`, `delete_record`, `index_document`, `send_notification`, `call_agent`)
- `ChatRequest` (message, context, execution_mode: `'direct'` | `'plan'`)
- `ChatResponse` (response, actions)
@@ -22,7 +22,7 @@
- `BillingTier` enum (`free`, `pro`, `power`, `team`)
- `AuthTokens` (access_token, refresh_token, expires_at)
- `UserProfile` (id, email, tier)
- [ ] Create `src/shared/batch-types.ts` with all types for the batch builder and storage layer:
- [x] Create `src/shared/batch-types.ts` with all types for the batch builder and storage layer:
- `StorageTarget``'local'` | `'cloud'` | `'sync'` | `'none'`
- `ConnectorType``'imap'` | `'filesystem'` | `'calendar'` | `'api'` | `'gmail'` | `'gdrive'` | `'outlook'`
- `BatchActionType``'create_record'` | `'update_record'` | `'delete_record'` | `'index_document'` | `'send_notification'` | `'call_agent'`
@@ -38,7 +38,7 @@
- `InstalledPlugin``{ listing: PluginListing, installedAt, enabled, storageConfig: BatchStorage }`
- `DataSourceInfo``{ type: ConnectorType, label, recordCount, sizeBytes, storageTarget: StorageTarget }`
- `StorageStats``{ localUsedBytes, cloudUsedBytes, cloudLimitBytes, sources: DataSourceInfo[] }`
- [ ] Update `tsconfig.json` paths if needed to include `src/shared/`
- [x] Update `tsconfig.json` paths if needed to include `src/shared/`
- **Files:** `src/shared/api-types.ts`, `src/shared/batch-types.ts`, `tsconfig.json`
- **Outcome:** Type-safe contracts for all backend communication and the batch/storage subsystem. Backend repo mirrors these as Pydantic schemas.

View File

@@ -39,6 +39,10 @@ interface OrchestrateInput {
sender?: Electron.WebContents;
}
/**
* @deprecated Superseded by `ChatResponse` from `@shared/api-types`.
* Will be replaced during Step 3.2 when the orchestrator delegates to the backend.
*/
interface OrchestrateResult {
response: string;
error?: string;

View File

@@ -546,6 +546,11 @@ const settingsRouter = router({
});
const aiRouter = router({
/**
* Chat mutation — local orchestration.
* The inline input schema mirrors `ChatRequest` from `@shared/api-types`.
* Will be replaced with the shared schema in Step 3.2.
*/
chat: publicProcedure
.input(z.object({
message: z.string(),

View File

@@ -4,7 +4,8 @@ import ReactMarkdown from 'react-markdown';
import remarkGfm from 'remark-gfm';
import { motion, AnimatePresence } from 'framer-motion';
import { trpc } from '@/lib/trpc';
import { useAIChat, type ChatContext } from '@/hooks/useAIChat';
import { useAIChat } from '@/hooks/useAIChat';
import type { ChatContext } from '@shared/api-types';
import { Button } from '@/components/ui/button';
import { Skeleton } from '@/components/ui/skeleton';
import { ScrollArea } from '@/components/ui/scroll-area';

View File

@@ -10,7 +10,8 @@ import {
CHAT_HEIGHT,
PADDING,
} from '@/context/FloatingChatContext';
import { useAIChat, type ChatContext } from '@/hooks/useAIChat';
import { useAIChat } from '@/hooks/useAIChat';
import type { ChatContext } from '@shared/api-types';
import { ChatMarkdown } from '@/components/ai/AIChatPanel';
import { Skeleton } from '@/components/ui/skeleton';
import { trpc } from '@/lib/trpc';

View File

@@ -1,5 +1,6 @@
import { useState, useCallback, useRef } from 'react';
import { trpc } from '@/lib/trpc';
import type { ChatContext } from '@shared/api-types';
interface ChatMessage {
id: string;
@@ -8,11 +9,7 @@ interface ChatMessage {
error?: boolean;
}
export interface ChatContext {
type: 'global' | 'project';
projectId?: string;
uiContext?: string;
}
export type { ChatContext };
interface UseAIChatReturn {
messages: ChatMessage[];

154
src/shared/api-types.ts Normal file
View File

@@ -0,0 +1,154 @@
/**
* Backend API contract types — shared between Electron main/renderer and the backend.
*
* Co-locates Zod schemas with inferred TypeScript types so the same definitions
* serve both compile-time type-safety and runtime validation of API responses.
*
* @see AI_REFACTOR_PLAN.md — Phase 0, Step 0.1
*/
import { z } from 'zod';
// ---------------------------------------------------------------------------
// Billing & Auth
// ---------------------------------------------------------------------------
export const BillingTierSchema = z.enum(['free', 'pro', 'power', 'team']);
export type BillingTier = z.infer<typeof BillingTierSchema>;
export const AuthTokensSchema = z.object({
accessToken: z.string(),
refreshToken: z.string(),
expiresAt: z.string().datetime(),
});
export type AuthTokens = z.infer<typeof AuthTokensSchema>;
export const UserProfileSchema = z.object({
id: z.string(),
email: z.string().email(),
tier: BillingTierSchema,
});
export type UserProfile = z.infer<typeof UserProfileSchema>;
// ---------------------------------------------------------------------------
// Execution Plans
// ---------------------------------------------------------------------------
export const PlanActionTypeSchema = z.enum([
'create_record',
'update_record',
'delete_record',
'index_document',
'send_notification',
'call_agent',
]);
export type PlanActionType = z.infer<typeof PlanActionTypeSchema>;
export const PlanActionSchema = z.object({
type: PlanActionTypeSchema,
table: z.string().optional(),
payload: z.record(z.string(), z.unknown()).optional(),
targetAgent: z.string().optional(),
});
export type PlanAction = z.infer<typeof PlanActionSchema>;
export const PlanStepSchema = z.object({
id: z.string(),
description: z.string(),
action: PlanActionSchema,
dependsOn: z.array(z.string()).optional(),
});
export type PlanStep = z.infer<typeof PlanStepSchema>;
export const ExecutionPlanSchema = z.object({
id: z.string(),
name: z.string(),
steps: z.array(PlanStepSchema),
createdAt: z.string().datetime(),
});
export type ExecutionPlan = z.infer<typeof ExecutionPlanSchema>;
// ---------------------------------------------------------------------------
// Chat — Unified Context
// ---------------------------------------------------------------------------
/**
* Unified chat context shared by renderer (UI) and backend (enriched).
*
* The base fields (`type`, `projectId`, `uiContext`) are used by the renderer
* and the current local orchestrator. The optional enriched fields are populated
* by the backend client (Phase 3) before forwarding to the cloud API.
*/
export const ChatContextSchema = z.object({
/** Scope of the conversation. */
type: z.enum(['global', 'project']),
/** Active project ID when `type === 'project'`. */
projectId: z.string().optional(),
/** Serialised description of the current UI state (visible section, selected item, etc.). */
uiContext: z.string().optional(),
// --- Enriched fields (populated before sending to backend) ----------------
/** User profile snapshot. */
userProfile: UserProfileSchema.optional(),
/** Relevant documents retrieved from the vector store. */
relevantDocuments: z
.array(z.object({ id: z.string(), content: z.string(), score: z.number().optional() }))
.optional(),
/** Recent tasks for additional context. */
recentTasks: z
.array(z.object({ id: z.string(), title: z.string(), status: z.string() }))
.optional(),
/** Previous messages in the conversation. */
conversationHistory: z
.array(z.object({ role: z.enum(['user', 'assistant', 'system']), content: z.string() }))
.optional(),
});
export type ChatContext = z.infer<typeof ChatContextSchema>;
// ---------------------------------------------------------------------------
// Chat Request / Response
// ---------------------------------------------------------------------------
export const ChatRequestSchema = z.object({
message: z.string(),
context: ChatContextSchema,
executionMode: z.enum(['direct', 'plan']).default('direct'),
});
export type ChatRequest = z.infer<typeof ChatRequestSchema>;
export const ChatResponseSchema = z.object({
response: z.string(),
actions: z.array(PlanActionSchema).optional(),
});
export type ChatResponse = z.infer<typeof ChatResponseSchema>;
// ---------------------------------------------------------------------------
// Agent Manifests & Permissions
// ---------------------------------------------------------------------------
export const AgentManifestSchema = z.object({
name: z.string(),
description: z.string(),
permissions: z.array(z.string()),
schedule: z.string().optional(),
});
export type AgentManifest = z.infer<typeof AgentManifestSchema>;
export const PermissionGrantSchema = z.object({
plugin: z.string(),
permissionType: z.string(),
resourcePath: z.string(),
grantedAt: z.string().datetime(),
});
export type PermissionGrant = z.infer<typeof PermissionGrantSchema>;
// ---------------------------------------------------------------------------
// Backup
// ---------------------------------------------------------------------------
export const BackupMetadataSchema = z.object({
version: z.number().int(),
timestamp: z.string().datetime(),
checksum: z.string(),
chunkCount: z.number().int(),
});
export type BackupMetadata = z.infer<typeof BackupMetadataSchema>;

172
src/shared/batch-types.ts Normal file
View File

@@ -0,0 +1,172 @@
/**
* Batch builder & storage layer contract types.
*
* Defines all types for the LLM-powered Batch Builder, plugin system,
* storage management, and data source connectors.
*
* @see AI_REFACTOR_PLAN.md — Phase 0, Step 0.1
*/
import { z } from 'zod';
// ---------------------------------------------------------------------------
// Storage
// ---------------------------------------------------------------------------
export const StorageTargetSchema = z.enum(['local', 'cloud', 'sync', 'none']);
export type StorageTarget = z.infer<typeof StorageTargetSchema>;
export const BatchStorageSchema = z.object({
/** Where structured records are persisted. */
records: StorageTargetSchema,
/** Where vector embeddings are stored. */
vectors: StorageTargetSchema,
/** Where raw/unprocessed data is kept. */
rawData: StorageTargetSchema,
});
export type BatchStorage = z.infer<typeof BatchStorageSchema>;
// ---------------------------------------------------------------------------
// Connectors
// ---------------------------------------------------------------------------
export const ConnectorTypeSchema = z.enum([
'imap',
'filesystem',
'calendar',
'api',
'gmail',
'gdrive',
'outlook',
]);
export type ConnectorType = z.infer<typeof ConnectorTypeSchema>;
// ---------------------------------------------------------------------------
// Batch Config Building Blocks
// ---------------------------------------------------------------------------
export const BatchActionTypeSchema = z.enum([
'create_record',
'update_record',
'delete_record',
'index_document',
'send_notification',
'call_agent',
]);
export type BatchActionType = z.infer<typeof BatchActionTypeSchema>;
export const BatchSourceSchema = z.object({
connector: ConnectorTypeSchema,
config: z.record(z.string(), z.unknown()),
});
export type BatchSource = z.infer<typeof BatchSourceSchema>;
export const BatchTriggerSchema = z.object({
type: z.enum(['cron', 'event']),
/** Cron expression (required when `type === 'cron'`). */
schedule: z.string().optional(),
/** IANA timezone identifier. Defaults to system timezone when omitted. */
timezone: z.string().optional(),
});
export type BatchTrigger = z.infer<typeof BatchTriggerSchema>;
export const BatchAnalysisSchema = z.object({
/** Natural-language prompt describing what to extract / classify / summarise. */
prompt: z.string(),
/** Override the default LLM model for this analysis step. */
modelOverride: z.string().optional(),
/** Optional JSON Schema describing the expected output shape. */
outputSchema: z.record(z.string(), z.unknown()).optional(),
});
export type BatchAnalysis = z.infer<typeof BatchAnalysisSchema>;
export const BatchActionSchema = z.object({
type: BatchActionTypeSchema,
/** Target table for record operations. */
table: z.string().optional(),
/** Field mapping from analysis output keys → table columns / action parameters. */
mapping: z.record(z.string(), z.string()).optional(),
});
export type BatchAction = z.infer<typeof BatchActionSchema>;
// ---------------------------------------------------------------------------
// Full Batch Config
// ---------------------------------------------------------------------------
export const BatchConfigSchema = z.object({
id: z.string(),
name: z.string(),
description: z.string(),
enabled: z.boolean(),
source: BatchSourceSchema,
trigger: BatchTriggerSchema,
analysis: BatchAnalysisSchema,
actions: z.array(BatchActionSchema),
storage: BatchStorageSchema,
/** Permission scopes this batch requires. Validated against PermissionManager at runtime. */
permissions: z.array(z.string()),
});
export type BatchConfig = z.infer<typeof BatchConfigSchema>;
// ---------------------------------------------------------------------------
// Batch Runtime
// ---------------------------------------------------------------------------
export const BatchStatusSchema = z.enum(['idle', 'running', 'error', 'disabled']);
export type BatchStatus = z.infer<typeof BatchStatusSchema>;
export const BatchRunResultSchema = z.object({
batchId: z.string(),
runAt: z.string().datetime(),
status: BatchStatusSchema,
itemsProcessed: z.number().int(),
errors: z.array(z.string()),
});
export type BatchRunResult = z.infer<typeof BatchRunResultSchema>;
// ---------------------------------------------------------------------------
// Plugin Marketplace
// ---------------------------------------------------------------------------
export const PluginListingSchema = z.object({
id: z.string(),
name: z.string(),
description: z.string(),
author: z.string(),
version: z.string(),
rating: z.number().min(0).max(5),
installs: z.number().int().min(0),
category: z.string(),
permissions: z.array(z.string()),
/** Price in cents. `0` means free. */
price: z.number().int().min(0),
});
export type PluginListing = z.infer<typeof PluginListingSchema>;
export const InstalledPluginSchema = z.object({
listing: PluginListingSchema,
installedAt: z.string().datetime(),
enabled: z.boolean(),
storageConfig: BatchStorageSchema,
});
export type InstalledPlugin = z.infer<typeof InstalledPluginSchema>;
// ---------------------------------------------------------------------------
// Data Manager
// ---------------------------------------------------------------------------
export const DataSourceInfoSchema = z.object({
type: ConnectorTypeSchema,
label: z.string(),
recordCount: z.number().int().min(0),
sizeBytes: z.number().int().min(0),
storageTarget: StorageTargetSchema,
});
export type DataSourceInfo = z.infer<typeof DataSourceInfoSchema>;
export const StorageStatsSchema = z.object({
localUsedBytes: z.number().int().min(0),
cloudUsedBytes: z.number().int().min(0),
cloudLimitBytes: z.number().int().min(0),
sources: z.array(DataSourceInfoSchema),
});
export type StorageStats = z.infer<typeof StorageStatsSchema>;

View File

@@ -14,7 +14,8 @@
"sourceMap": true,
"baseUrl": ".",
"paths": {
"@/*": ["src/renderer/*"]
"@/*": ["src/renderer/*"],
"@shared/*": ["src/shared/*"]
},
"outDir": "dist"
},

View File

@@ -1,7 +1,13 @@
import { defineConfig } from 'vite';
import path from 'path';
// https://vitejs.dev/config
export default defineConfig({
resolve: {
alias: {
'@shared': path.resolve(__dirname, './src/shared'),
},
},
build: {
rollupOptions: {
// Externalize native Node modules — they're rebuilt by electron-forge

View File

@@ -17,6 +17,7 @@ export default defineConfig({
resolve: {
alias: {
'@': path.resolve(__dirname, './src/renderer'),
'@shared': path.resolve(__dirname, './src/shared'),
},
},
});