step 0.1 complete: Type-safe contracts for all backend communication and the batch/storage subsystem
This commit is contained in:
@@ -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.
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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
154
src/shared/api-types.ts
Normal 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
172
src/shared/batch-types.ts
Normal 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>;
|
||||
@@ -14,7 +14,8 @@
|
||||
"sourceMap": true,
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@/*": ["src/renderer/*"]
|
||||
"@/*": ["src/renderer/*"],
|
||||
"@shared/*": ["src/shared/*"]
|
||||
},
|
||||
"outDir": "dist"
|
||||
},
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -17,6 +17,7 @@ export default defineConfig({
|
||||
resolve: {
|
||||
alias: {
|
||||
'@': path.resolve(__dirname, './src/renderer'),
|
||||
'@shared': path.resolve(__dirname, './src/shared'),
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user