UNPKG

oneie

Version:

Build apps, websites, and AI agents in English. Zero-interaction setup for AI agents (Claude Code, Cursor, Windsurf). Download to your computer, run in the cloud, deploy to the edge. Open source and free forever.

1,745 lines (1,470 loc) 71.9 kB
--- title: Ontology Frontend dimension: knowledge category: ontology-frontend.md tags: 6-dimensions, agent, architecture, backend, frontend, ontology, testing, ui related_dimensions: connections, events, groups, people, things scope: global created: 2025-11-03 updated: 2025-11-03 version: 1.0.0 ai_context: | This document is part of the knowledge dimension in the ontology-frontend.md category. Location: one/knowledge/ontology-frontend.md Purpose: Documents frontend development ontology Related dimensions: connections, events, groups, people, things For AI agents: Read this to understand ontology frontend. --- # Frontend Development Ontology **Version:** 2.0.0 **Type System:** Formal ontology for backend-agnostic Astro website development **Paradigm:** Pure declarative type theory + Provider pattern + Context engineering --- ## Table of Contents 1. [Core Axioms](#core-axioms) 2. [Provider Pattern & Context Engineering](#provider-pattern--context-engineering) 3. [Type Hierarchy](#type-hierarchy) 4. [Critical Distinctions](#critical-distinctions) 5. [State Management Hierarchy](#state-management-hierarchy) 6. [DataProvider Interface](#dataprovider-interface-universal-api) 7. [Entity Definitions](#entity-definitions) 8. [Multi-Tenant Architecture](#multi-tenant-architecture) 9. [Caching Ontology](#caching-ontology) 10. [Error Propagation Ontology](#error-propagation-ontology) 11. [Testing Ontology](#testing-ontology) 12. [Pattern Language](#pattern-language) 13. [Type Sync Automation](#type-sync-automation) 14. [Observability Layer](#observability-layer) 15. [Agent Task Mapping](#agent-task-mapping) 16. [Common Mistakes](#common-mistakes) 17. [Summary](#summary) --- ## Core Axioms ### Axiom 1: Everything is a Thing ``` ∀x ∈ Frontend → x : Thing Thing ::= Page | Component | Content | Service | Provider | Configuration ``` ### Axiom 2: All Things Have Type ``` type : Thing → TypeID TypeID ::= String ∈ ontology.things.types ``` ### Axiom 3: Things Connect ``` connect : Thing × Thing → Connection Connection ::= { from: Thing, to: Thing, type: RelationType, metadata: Object } ``` ### Axiom 4: Actions Emit Events ``` action : Thing → Event Event ::= { type: EventType, actor: Thing, target: Thing, timestamp: Time, metadata: Object } ``` ### Axiom 5: Patterns Compose ``` compose : Pattern × Pattern → Pattern Pattern ::= { type: PatternType, inputs: [Thing], outputs: [Thing], transform: Function } ``` ### Axiom 6: Backend Agnosticism ``` ∀Provider. Provider implements DataProviderInterface → Frontend works with Provider DataProviderInterface ::= { organizations, people, things, connections, events, knowledge } ``` ### Axiom 7: Context Minimalism ``` ∀Operation. Load only required types, not implementations ContextUsed << ContextAvailable target_reduction: 98%+ ``` **Key Principle:** Frontend knows 6-dimension ontology, not backend implementation. --- ## Provider Pattern & Context Engineering ### The Core Insight: Providers ARE Context Loaders ```typescript { insight: "Provider pattern = 99.9% context reduction" // ❌ Traditional: Load entire backend traditionalApproach: { load: "Full Convex schema + all implementations + all docs", tokens: 280_000, problem: "Frontend context explodes with backend knowledge" } // ✅ Provider pattern: Load only interface providerApproach: { load: "DataProviderInterface only (contract)", tokens: 300, benefit: "Frontend never loads backend implementation" } // Backend translation happens at runtime (0 frontend tokens) execution: { frontend: "provider.things.get(id)", // Uses interface provider: "convexClient.query(...)", // Translates to backend backend: "Database query", // Executes result: "Frontend context = 300 tokens (not 280k)" } result: { context_reduction: "99.9%", backend_flexibility: "infinite (swap with config)", type_safety: "Effect.ts typed errors", testability: "Mock provider for tests" } } ``` ### Provider as Just-In-Time Loader ```typescript // Traditional AI code generation ❌ function generateCourseComponent_Traditional() { context = { convexSchema: loadConvexSchema(), // 15,000 tokens convexMutations: loadMutations(), // 20,000 tokens convexQueries: loadQueries(), // 18,000 tokens implementations: loadImplementations() // 50,000 tokens } // Total: 103,000 tokens return ai.generate(task, context) } // ✅ Provider pattern ✅ function generateCourseComponent_Provider() { context = { interface: "DataProviderInterface", // 300 tokens operations: ["things.get", "things.list"] // Which operations needed } // Total: 300 tokens // AI generates code using interface code = ai.generate(task, context) // Result: provider.things.get(id) - works with ANY backend return code } // Savings: 103,000 → 300 tokens = 99.7% reduction ``` ### Context Engineering Formula ```typescript { traditional: { formula: "ContextSize = Σ(all_files + all_docs + all_examples)", typical: "50k-300k tokens per request", problem: "Hits context limits, slow, expensive" }, provider: { formula: "ContextSize = interface_definition + operation_signatures", typical: "300-500 tokens per request", benefit: "Never hits limits, fast, cheap, infinite backends" }, improvement: { context_reduction: "99%+", cost_reduction: "100x cheaper", speed_improvement: "10x faster", backend_flexibility: "∞ backends supported" } } ``` --- ## Type Hierarchy ### Base Types ``` Thing ├── Artifact // Code artifacts (what agents create) │ ├── Page │ │ ├── LandingPage │ │ ├── BlogIndex │ │ ├── BlogPost │ │ ├── AppPage │ │ ├── AccountPage │ │ └── APIRoute │ ├── Component │ │ ├── UIComponent │ │ ├── FeatureComponent │ │ ├── Layout │ │ └── Island │ ├── Content │ │ ├── Collection │ │ ├── Entry │ │ └── Schema │ ├── Service // Effect.ts services (type-safe data operations) │ │ ├── GenericService // Handles all 66 types (ThingService, ConnectionService) │ │ └── SpecializedService // Optional convenience (CourseService) │ ├── Provider // Backend implementations (swap backends) │ │ ├── ConvexProvider // Convex backend │ │ ├── WordPressProvider // WordPress REST API │ │ ├── NotionProvider // Notion databases │ │ ├── SupabaseProvider // Supabase PostgreSQL │ │ └── CustomProvider // Any backend │ └── Configuration // Display & provider config (NOT business logic) │ ├── DisplayConfig // UI labels, icons, colors │ └── ProviderConfig // Backend URL, API keys ├── Pattern // Reusable templates │ ├── PagePattern │ ├── ComponentPattern │ ├── DataPattern │ ├── ServicePattern │ └── StylePattern ├── Interface // Abstract contracts │ ├── DataProviderInterface // Universal 6-dimension API │ └── SubscriptionInterface // Real-time updates (optional) ├── Capability // Agent capabilities │ ├── Create │ ├── Read │ ├── Update │ └── Delete └── Context // Execution context ├── TypeContext ├── PatternContext └── ExampleContext ``` --- ## Critical Distinctions ### What Frontend IS ```typescript type FrontendResponsibility = | { type: "render"; data: any } // Display UI from data | { type: "call_provider"; operation: DataOperation } // Call DataProvider (not direct backend) | { type: "manage_ui_state"; state: UIState } // Form inputs, modals, loading (component-local) | { type: "route"; path: Path } // Client-side navigation | { type: "cache_display"; data: CachedData }; // Cache UI data (not business data) ``` ### What Frontend IS NOT ```typescript type FrontendProhibition = | { type: "database_access"; reason: "Backend-only (provider abstracts)" } | { type: "business_logic"; reason: "Backend validates (frontend calls provider)"; } | { type: "event_logging"; reason: "Backend logs (frontend just triggers)" } | { type: "authorization"; reason: "Backend authorizes (frontend just checks state)"; } | { type: "data_validation"; reason: "Backend validates (frontend UX hints only)"; } | { type: "org_filtering"; reason: "Provider handles (frontend never filters by org)"; } | { type: "backend_specific_code"; reason: "Provider abstracts (frontend uses interface)"; }; ``` --- ## State Management Hierarchy ### Four-Layer State Architecture ```typescript { // Level 1: Server State (Provider owns - SOURCE OF TRUTH) ServerState: { owner: "Provider (backed by database)", source: "Backend database", access: "Query/Mutation via provider.interface only", examples: [ "course list", "user profile", "enrollment count", "lesson progress" ], caching: "Provider-level (Cloudflare KV, Edge cache)", lifetime: "Persistent (until backend changes)", rule: "NEVER duplicate in frontend state. Query when needed." }, // Level 2: SSR State (Astro owns - REQUEST SCOPED) SSRState: { owner: "Astro page (.astro files)", source: "Server-side provider fetch", access: "Props passed to components", examples: [ "Initial page data (course details)", "SEO metadata (title, description)", "Organization context (from subdomain)" ], lifetime: "One HTTP request (not persisted)", pattern: ` --- const course = await provider.things.get(id) --- <Layout title={course.name}> <CourseDetail course={course} /> </Layout> `, rule: "Use for SEO, initial render, server-only data" }, // Level 3: Island State (React owns - COMPONENT SCOPED) IslandState: { owner: "React component (useState/useReducer)", source: "Component-local logic", access: "Component-private (not shared)", examples: [ "Form input values", "Modal open/closed", "Dropdown expanded", "Loading spinner visible" ], lifetime: "Component mounted (unmount = state destroyed)", pattern: ` export function CourseForm() { const [title, setTitle] = useState('') const [loading, setLoading] = useState(false) // State dies when component unmounts } `, rule: "UI-only state. Never duplicate server state." }, // Level 4: Shared Client State (Nanostores owns - SESSION SCOPED) SharedState: { owner: "Nanostores (global atoms)", source: "Cross-component coordination", access: "Multiple components subscribe", examples: [ "Sidebar expanded/collapsed", "Dark/light theme preference", "User locale (en/es/fr)", "Toast notifications" ], lifetime: "Browser session + localStorage", pattern: ` // stores/layout.ts export const sidebarExpanded = atom(true) // Component A const expanded = useStore(sidebarExpanded) // Component B (synced with A) const expanded = useStore(sidebarExpanded) `, rule: "UI preferences only. NOT server data." }, // Anti-Pattern: Duplicating Server State antiPattern: { problem: "Storing server data in frontend state", example: ` ❌ const [courses, setCourses] = useState([]) ❌ useEffect(() => { provider.things.list('course').then(setCourses) }, []) // Problem: Server state duplicated in frontend // Problem: Stale data, manual sync, cache invalidation `, solution: ` ✅ const courses = useQuery(api.courses.list) // Provider handles caching, revalidation, subscriptions // Frontend just renders. No state duplication. ` }, // Golden Rule rule: "State lives at HIGHEST appropriate level. Don't lift unnecessarily." // Decision Tree decisionTree: { question: "Does this state persist after page reload?", yes: { question: "Is this data from backend?", yes: "ServerState (provider query)", no: "SharedState (nanostores + localStorage)" }, no: { question: "Do multiple components need this state?", yes: "SharedState (nanostores, session-only)", no: { question: "Is this SSR data?", yes: "SSRState (Astro props)", no: "IslandState (useState)" } } } } ``` --- ## DataProvider Interface (Universal API) ### Core Contract ```typescript // ALL backends must implement this interface interface DataProviderInterface { // Dimension 1: Organizations organizations: { get: (id: string) → Effect<Organization, OrganizationNotFoundError> list: (params: ListParams) → Effect<Organization[], Error> update: (id: string, updates: Partial<Organization>) → Effect<void, Error> } // Dimension 2: People people: { get: (id: string) → Effect<Person, PersonNotFoundError | UnauthorizedError> list: (params: ListParams) → Effect<Person[], Error> create: (input: CreatePersonInput) → Effect<string, PersonCreateError> update: (id: string, updates: Partial<Person>) → Effect<void, Error> delete: (id: string) → Effect<void, Error> } // Dimension 3: Things things: { get: (id: string) → Effect<Thing, ThingNotFoundError | UnauthorizedError> list: (params: { type: ThingType; groupId?: string; filters?: any }) → Effect<Thing[], Error> create: (input: CreateThingInput) → Effect<string, ThingCreateError> update: (id: string, updates: Partial<Thing>) → Effect<void, Error> delete: (id: string) → Effect<void, Error> } // Dimension 4: Connections connections: { create: (input: CreateConnectionInput) → Effect<string, ConnectionCreateError> getRelated: (params: { thingId: string; relationshipType: ConnectionType; direction: Direction }) → Effect<Thing[], Error> getCount: (thingId: string, relationshipType: ConnectionType) → Effect<number, Error> delete: (id: string) → Effect<void, Error> } // Dimension 5: Events events: { log: (event: LogEventInput) → Effect<void, Error> query: (params: EventQueryParams) → Effect<Event[], Error> } // Dimension 6: Knowledge knowledge: { embed: (params: EmbedParams) → Effect<string, Error> search: (params: SearchParams) → Effect<KnowledgeMatch[], Error> } // Optional: Real-time subscriptions (not all backends support) subscriptions?: { watchThing: (id: string) → Effect<Observable<Thing>, Error> watchList: (type: ThingType, groupId?: string) → Effect<Observable<Thing[]>, Error> } } ``` ### Provider Algebra ```typescript // Provider composition implement : Backend × DataProviderInterface → Provider // Examples: ConvexProvider = implement(ConvexBackend, DataProviderInterface) WordPressProvider = implement(WordPressBackend, DataProviderInterface) NotionProvider = implement(NotionBackend, DataProviderInterface) // Swapping backends (change ONE line in config) config.provider = ConvexProvider({ url: "..." }) // OR config.provider = WordPressProvider({ url: "...", apiKey: "..." }) // OR config.provider = NotionProvider({ apiKey: "...", databaseId: "..." }) // Result: Frontend components don't change ``` --- ## Entity Definitions ### Services (Effect.ts Layer) ```typescript // Generic Service (handles ALL 66 thing types) type GenericService = Service & { subtype: "generic" coverage: ThingType[] // All 66 types operations: CRUD invariant: { backend_agnostic: true // Works with ANY provider type_safe: true // Effect.ts typed errors composable: true // Combines into workflows } } // Example: ThingService handles courses, lessons, products, EVERYTHING type ThingService = GenericService & { name: "ThingService" operations: { get: (id: string) → Effect<Thing, ThingNotFoundError> list: (type: ThingType, orgId?: string) → Effect<Thing[], Error> create: (input: CreateThingInput) → Effect<string, ThingCreateError> update: (id: string, updates: any) → Effect<void, Error> delete: (id: string) → Effect<void, Error> } // Delegates to configured provider implementation: (provider: DataProvider) => ({ get: (id) => provider.things.get(id), list: (type, groupId) => provider.things.list({ type, groupId }), // ... }) } // Example: ConnectionService handles ALL relationship types type ConnectionService = GenericService & { name: "ConnectionService" operations: { create: (input: CreateConnectionInput) → Effect<string, ConnectionCreateError> getRelated: (thingId, type, direction) → Effect<Thing[], Error> getCount: (thingId, type) → Effect<number, Error> delete: (id) → Effect<void, Error> } implementation: (provider: DataProvider) => ({ create: (input) => provider.connections.create(input), getRelated: (thingId, type, dir) => provider.connections.getRelated({ thingId, relationshipType: type, direction: dir }), // ... }) } // Specialized Service (OPTIONAL - only add when patterns repeat 3+ times) type SpecializedService = Service & { subtype: "specialized" coverage: ThingType[] // Specific types only operations: DomainOperations dependencies: GenericService[] // Composes generic services invariant: { adds_convenience: true // Clearer domain vocabulary reduces_repetition: true // Encapsulates multi-step operations optional: true // NOT required (can use generic services directly) } } // Example: CourseService (OPTIONAL - only if you repeat these patterns) type CourseService = SpecializedService & { name: "CourseService" coverage: ["course"] dependencies: [ThingService, ConnectionService] operations: { // Convenience: Get course with lessons (vs 2 separate calls) getCourseWithLessons: (courseId) → Effect<{ course: Thing; lessons: Thing[] }, Error> // Convenience: Domain vocabulary (vs generic connection.create) enrollUser: (userId, courseId) → Effect<string, Error> // Convenience: Specific query (vs generic connection.getCount) getEnrollmentCount: (courseId) → Effect<number, Error> } implementation: (thingService, connectionService) => ({ getCourseWithLessons: (id) => Effect.all([ thingService.get(id), connectionService.getRelated(id, 'part_of', 'to') ]), enrollUser: (userId, courseId) => connectionService.create({ fromThingId: userId, toThingId: courseId, relationshipType: 'enrolled_in' }), getEnrollmentCount: (courseId) => connectionService.getCount(courseId, 'enrolled_in') }) } ``` ### Providers (Backend Implementations) ```typescript type Provider = { type: "provider" backend: Backend // Convex, WordPress, Notion, etc implements: DataProviderInterface // Map ontology operations → backend-specific API calls translation: { things: { get: (id) → BackendSpecificCall list: (params) → BackendSpecificCall create: (input) → BackendSpecificCall // ... } connections: { // ... } // ... all 6 dimensions } invariant: { implements_interface: true // Must implement DataProviderInterface handles_6_dimensions: true // Organizations, People, Things, Connections, Events, Knowledge } } // Convex Provider type ConvexProvider = Provider & { backend: "convex" translation: { things: { get: (id) => convexClient.query(api.queries.things.get, { id }), list: (params) => convexClient.query(api.queries.things.list, params), create: (input) => convexClient.mutation(api.mutations.things.create, input), // ... } // ... map all operations to Convex API } } // WordPress Provider type WordPressProvider = Provider & { backend: "wordpress" translation: { things: { get: (id) => fetch(`${url}/wp-json/wp/v2/posts/${id}`) .then(transformWordPressPost → OneThing), list: (params) => fetch(`${url}/wp-json/wp/v2/posts?per_page=${params.limit}`) .then(transformWordPressPosts → OneThings), create: (input) => fetch(`${url}/wp-json/wp/v2/posts`, { method: 'POST', body: transformOneThing → WordPressPost }), // ... } // ... map all operations to WordPress REST API } } // Notion Provider type NotionProvider = Provider & { backend: "notion" translation: { things: { get: (id) => notionClient.pages.retrieve({ page_id: id }) .then(transformNotionPage → OneThing), list: (params) => notionClient.databases.query({ database_id, page_size: params.limit }) .then(transformNotionPages → OneThings), create: (input) => notionClient.pages.create({ parent: { database_id }, properties: transformOneThing → NotionProperties }), // ... } // ... map all operations to Notion API } } ``` ### Configuration ```typescript // Display Configuration (UI only, NOT business logic) type DisplayConfig = Configuration & { purpose: "ui_presentation"; scope: ThingType[]; properties: { displayName: string; // "Course" (singular) displayNamePlural: string; // "Courses" (plural) icon: LucideIconName; // "BookOpen" color: TailwindColor; // "green" primaryField: string; // Which property to show as title secondaryField: string; // Which property to show as subtitle imageField: string; // Which property is the image fields: FieldConfig[]; // Form fields for UI }; invariant: { ui_only: true; // NO business logic backend_agnostic: true; // Works with any provider display_hints: true; // Labels, placeholders, icons }; }; // Provider Configuration (backend connection) type ProviderConfig = Configuration & { purpose: "backend_connection"; properties: { provider: ProviderType; // "convex" | "wordpress" | "notion" | etc url: string; // Backend URL apiKey?: string; // API key (if needed) databaseId?: string; // Database ID (for Notion) // ... other backend-specific config }; invariant: { one_line_swap: true; // Change provider in ONE line environment_vars: true; // Use .env for secrets }; }; ``` --- ## Multi-Tenant Architecture ### Subdomain-Based Organization Isolation ```typescript { pattern: "subdomain_org_isolation", problem: "How does provider handle fitnesspro.one.ie vs techcorp.one.ie?", solution: { // Middleware extracts orgId from subdomain middleware: ` // src/middleware.ts export function onRequest(context, next) { const subdomain = context.url.hostname.split('.')[0] const group = await provider.groups.get({ slug: subdomain }) context.locals.groupId = group._id context.locals.group = group return next() } `, // Provider auto-injects groupId in ALL queries provider: ` // Provider automatically scopes ALL queries class ConvexProvider implements DataProviderInterface { constructor(private ctx: Context) {} things = { list: (params) => { // Provider ALWAYS adds org filter (frontend never needs to) return this.client.query(api.things.list, { ...params, groupId: this.ctx.locals.groupId // Auto-injected }) }, create: (input) => { // Provider ALWAYS stamps org on creation return this.client.mutation(api.things.create, { ...input, properties: { ...input.properties, groupId: this.ctx.locals.groupId // Auto-injected } }) } } } `, // Frontend NEVER manually filters by org frontend: ` // ✅ Frontend code (same for all orgs) const courses = await provider.things.list({ type: 'course' }) // Provider adds groupId automatically // ❌ NEVER do this const courses = await provider.things.list({ type: 'course', groupId: ctx.locals.groupId // Provider handles this }) `, invariants: [ "Provider enforces isolation (not frontend)", "Frontend never knows about multi-tenancy", "One frontend codebase for all groups", "Data isolation enforced at provider level" ] }, benefits: [ "Frontend code 100% group-agnostic", "Security enforced by provider (not UI)", "No group-specific frontend builds", "Impossible to leak group data (provider enforces)" ] } ``` --- ## Caching Ontology ### Four-Tier Caching Architecture ```typescript { // Tier 1: Type Definition Cache (STATIC - INFINITE TTL) L1_TypeDefinitions: { what: "Ontology type structures", ttl: "∞ (types never change at runtime)", storage: "Memory (startup load)", size: "200 tokens × 66 types = 13,200 tokens", hitRate: "99.9%+ (loaded once)", implementation: ` class TypeDefinitionCache { private cache = new Map<ThingType, TypeDefinition>() async load() { // Load all 66 types at startup (one time) const types = await import('./ontology/types') types.forEach(type => this.cache.set(type.name, type)) } get(type: ThingType): TypeDefinition { return this.cache.get(type) // Always hits (loaded at startup) } } // Usage: ~13k tokens loaded ONCE, reused for ALL requests const typeDef = typeCache.get('course') // 200 tokens, instant `, benefit: "AI loads 200 tokens (not 15k implementation tokens)" }, // Tier 2: Provider Interface Cache (STATIC - INFINITE TTL) L2_ProviderInterface: { what: "DataProvider method signatures", ttl: "∞ (interface never changes)", storage: "Memory (startup load)", size: "300 tokens (entire interface)", hitRate: "100% (never changes)", implementation: ` // Loaded once at startup const providerInterface = { things: { get: "(id: string) → Effect<Thing, Error>", list: "(params) → Effect<Thing[], Error>", // ... } // ... all 6 dimensions } // 300 tokens total // Usage: AI references interface (not backend implementation) context = { interface: providerInterface } // 300 tokens `, benefit: "AI never loads backend implementation (0 tokens)" }, // Tier 3: SSR Data Cache (DYNAMIC - TTL: 1-60s) L3_SSRData: { what: "Server-rendered page data", ttl: "1-60s (configurable per route)", storage: "Cloudflare KV / Edge cache", size: "Variable (full pages or data)", strategy: "Stale-while-revalidate", implementation: ` // Astro page with edge caching export const prerender = false export async function GET(context) { const cacheKey = \`course:\${context.params.id}\` // Check cache const cached = await context.locals.cache.get(cacheKey) if (cached && !isStale(cached, 60)) { return cached } // Fetch fresh const course = await provider.things.get(context.params.id) // Cache for 60s await context.locals.cache.set(cacheKey, course, { ttl: 60 }) return course } `, patterns: { high_traffic_pages: "60s TTL (homepage, popular courses)", dynamic_pages: "10s TTL (user dashboards)", real_time_pages: "0s TTL (admin, analytics)" }, benefit: "Reduces provider calls by 80-95%" }, // Tier 4: Client Subscriptions (REALTIME - TTL: 0) L4_RealtimeSubscriptions: { what: "WebSocket live updates", ttl: "0 (always fresh)", storage: "Client memory (ephemeral)", strategy: "Provider pushes updates", implementation: ` // React component with real-time subscription export function EnrollmentCount({ courseId }) { // Convex subscription (or any provider with subscription support) const count = useQuery(api.connections.getCount, { thingId: courseId, relationshipType: 'enrolled_in' }) // Updates automatically when backend changes return <span>{count}</span> } `, when_to_use: [ "Real-time dashboards", "Live enrollment counts", "Collaborative editing", "Chat/notifications" ], fallback: "If provider doesn't support subscriptions, poll with L3 cache" }, // Cache Hierarchy Decision Tree decisionTree: { question: "Is this ontology type data?", yes: "L1 (Type Definition Cache) - infinite TTL", no: { question: "Is this provider interface?", yes: "L2 (Provider Interface Cache) - infinite TTL", no: { question: "Does data change in real-time?", yes: "L4 (Real-time Subscriptions) - 0 TTL", no: { question: "Is data page-level?", yes: "L3 (SSR Data Cache) - 1-60s TTL", no: "No cache (fetch on demand)" } } } }, // Golden Rule rule: "Cache closer to definition = less context loaded. Types/interface = cached forever." } ``` ### Cache Invalidation Strategies ```typescript { strategies: { // L1 & L2: Never invalidate (static types/interface) static: { invalidation: "None (only invalidate on deployment)", reason: "Types and interface don't change at runtime" }, // L3: Time-based + event-based ssr: { time_based: "TTL expires → fetch fresh", event_based: "Backend mutation → invalidate affected pages", example: ` // When course updated, invalidate course page cache await provider.things.update(courseId, updates) await cache.delete(\`course:\${courseId}\`) ` }, // L4: Automatic (subscription handles) realtime: { invalidation: "Provider pushes updates automatically", reason: "WebSocket connection = always fresh" } } } ``` --- ## Error Propagation Ontology ### Full Error Flow: Provider → Service → Component → UI ```typescript { errorFlow: "Provider → Service → Component → UI", // Level 1: Provider Errors (Effect.ts tagged errors) providerErrors: { ThingNotFoundError: { _tag: "ThingNotFoundError", fields: { thingId: string; type: ThingType }, recovery: "Show 404 page or 'Not Found' component", httpStatus: 404 }, UnauthorizedError: { _tag: "UnauthorizedError", fields: { userId?: string; requiredPermission: string }, recovery: "Redirect to /login with returnUrl", httpStatus: 401 }, NetworkError: { _tag: "NetworkError", fields: { message: string; retryable: boolean }, recovery: "Retry with exponential backoff (if retryable)", httpStatus: 503 }, RateLimitError: { _tag: "RateLimitError", fields: { retryAfter: number }, recovery: "Show 'Try again in X seconds' message", httpStatus: 429 } }, // Level 2: Service Errors (Domain-specific, compose provider errors) serviceErrors: { pattern: "Transform provider error → domain error", example: ` // CourseService.ts getCourseWithLessons(id: string) { return Effect.gen(function* () { const course = yield* thingService.get(id) const lessons = yield* connectionService.getRelated(id, 'part_of', 'to') return { course, lessons } }).pipe( // Transform provider error → domain error Effect.catchTag('ThingNotFoundError', (e) => Effect.fail(new CourseNotFoundError({ courseId: e.thingId, message: \`Course \${e.thingId} not found\` })) ) ) } `, benefits: [ "Domain-specific error vocabulary", "Hides provider implementation details", "Easier for frontend to handle" ] }, // Level 3: Component Errors (Catch and render) componentErrors: { pattern: "Match error._tag → Render UI component", example: ` // CourseDetail.tsx export function CourseDetail({ courseId }: Props) { const [course, setCourse] = useState<Thing | null>(null) const [error, setError] = useState<Error | null>(null) useEffect(() => { Effect.runPromise( courseService.getCourseWithLessons(courseId) ) .then(result => setCourse(result.course)) .catch(e => setError(e)) }, [courseId]) // Render based on error type if (error) { switch (error._tag) { case 'CourseNotFoundError': return <Alert variant="warning">Course not found</Alert> case 'UnauthorizedError': return <Alert variant="error">Please log in to view</Alert> case 'NetworkError': return <Alert variant="error">Network error. Retrying...</Alert> default: return <Alert variant="error">Something went wrong</Alert> } } return <div>{/* Render course */}</div> } ` }, // Level 4: UI Errors (User-facing messages) uiErrors: { pattern: "Error → User-friendly message + Action", mapping: { ThingNotFoundError: { message: "This {type} doesn't exist", action: "Go back or search for another", component: "<Alert404 />" }, UnauthorizedError: { message: "You need to log in to see this", action: "Log in or Sign up", component: "<LoginPrompt returnUrl={currentPath} />" }, NetworkError: { message: "Connection issue", action: "Retrying automatically...", component: "<RetryAlert retrying={true} />" }, RateLimitError: { message: "Too many requests", action: "Try again in {retryAfter} seconds", component: "<RateLimitAlert retryAfter={error.retryAfter} />" } } }, // Error Boundary Pattern errorBoundary: ` // ErrorBoundary.tsx export class ErrorBoundary extends React.Component { state = { error: null } static getDerivedStateFromError(error) { return { error } } render() { if (this.state.error) { return <ErrorFallback error={this.state.error} /> } return this.props.children } } // Usage: Wrap islands in error boundaries <ErrorBoundary> <CourseDetail client:load courseId={id} /> </ErrorBoundary> `, // Golden Rule invariant: "Every error path has explicit UI recovery. No silent failures." } ``` --- ## Testing Ontology ### Three-Layer Testing Strategy ```typescript { // Layer 1: Unit Tests (Service logic with mock provider) unitTests: { what: "Test service logic in isolation", mock: "MockProvider (no real backend)", fast: "Milliseconds per test", example: ` // tests/unit/services/course.test.ts import { describe, it, expect } from 'vitest' import { Effect, Layer } from 'effect' import { CourseService } from '@/services/CourseService' import { DataProvider } from '@/providers/DataProvider' describe('CourseService.getCourseWithLessons', () => { it('should return course with lessons', async () => { // Mock provider const MockProvider = Layer.succeed(DataProvider, { things: { get: (id) => Effect.succeed({ _id: id, type: 'course', name: 'Test Course' }) }, connections: { getRelated: (thingId, type, direction) => Effect.succeed([ { _id: 'lesson1', type: 'lesson', name: 'Lesson 1' }, { _id: 'lesson2', type: 'lesson', name: 'Lesson 2' } ]) } }) // Test service const result = await Effect.runPromise( Effect.gen(function* () { const service = yield* CourseService return yield* service.getCourseWithLessons('course123') }).pipe(Effect.provide(MockProvider)) ) expect(result.course.name).toBe('Test Course') expect(result.lessons).toHaveLength(2) }) it('should fail with CourseNotFoundError', async () => { const MockProvider = Layer.succeed(DataProvider, { things: { get: (id) => Effect.fail(new ThingNotFoundError({ thingId: id })) } }) const result = await Effect.runPromiseExit( Effect.gen(function* () { const service = yield* CourseService return yield* service.getCourseWithLessons('invalid') }).pipe(Effect.provide(MockProvider)) ) expect(result._tag).toBe('Failure') expect(result.cause.error._tag).toBe('CourseNotFoundError') }) }) `, coverage: [ "Service logic (business rules)", "Error transformations", "Effect composition", "Edge cases" ] }, // Layer 2: Integration Tests (Service + Real Provider + Test Backend) integrationTests: { what: "Test service + provider + real backend", backend: "Test Convex deployment (isolated database)", slow: "Seconds per test (real network calls)", example: ` // tests/integration/course-creation.test.ts import { describe, it, expect, beforeEach, afterEach } from 'vitest' import { convexProvider } from '@/providers/convex' import { CourseService } from '@/services/CourseService' describe('Course Creation Flow', () => { let testProvider let testOrgId beforeEach(async () => { // Use test Convex deployment testProvider = convexProvider({ url: process.env.TEST_CONVEX_URL }) // Create test org testOrgId = await testProvider.organizations.create({ name: 'Test Org', slug: 'test-org' }) }) afterEach(async () => { // Cleanup test data await testProvider.organizations.delete(testOrgId) }) it('should create course with lessons', async () => { // Create course (real mutation) const courseId = await testProvider.things.create({ type: 'course', name: 'Integration Test Course', properties: { /* thing properties */ } }) // Create lessons (real mutations) const lesson1Id = await testProvider.things.create({ type: 'lesson', name: 'Lesson 1', properties: { /* thing properties */ } }) await testProvider.connections.create({ fromThingId: lesson1Id, toThingId: courseId, relationshipType: 'part_of' }) // Verify (real query) const result = await CourseService.getCourseWithLessons(courseId) expect(result.course.name).toBe('Integration Test Course') expect(result.lessons).toHaveLength(1) expect(result.lessons[0].name).toBe('Lesson 1') }) }) `, coverage: [ "Full data flow (service → provider → backend → database)", "Real mutations and queries", "Data consistency", "Backend constraints" ] }, // Layer 3: E2E Tests (Full UI flow in browser) e2eTests: { what: "Test complete user flows in browser", tool: "Playwright (headless Chrome)", slow: "10-30s per test (full page loads)", example: ` // tests/e2e/course-enrollment.spec.ts import { test, expect } from '@playwright/test' test.describe('Course Enrollment Flow', () => { test('user can enroll in course', async ({ page }) => { // Navigate to course page await page.goto('/courses/course123') // Verify course details rendered await expect(page.locator('h1')).toContainText('Test Course') // Click enroll button await page.click('button:has-text("Enroll")') // Wait for enrollment confirmation await expect(page.locator('.toast')).toContainText('Enrolled successfully') // Verify button changed to "Go to Course" await expect(page.locator('button')).toContainText('Go to Course') // Navigate to user dashboard await page.goto('/account') // Verify course appears in "My Courses" await expect(page.locator('.my-courses')).toContainText('Test Course') }) test('unauthorized user prompted to login', async ({ page }) => { // Navigate as guest await page.goto('/courses/course123') // Click enroll await page.click('button:has-text("Enroll")') // Should redirect to login await expect(page).toHaveURL(/\/login/) // Login form should have returnUrl await expect(page.locator('form')).toHaveAttribute( 'data-return-url', '/courses/course123' ) }) }) `, coverage: [ "User flows (click, type, navigate)", "Visual rendering", "Loading states", "Error states", "Authentication flows" ] }, // Test Pyramid pyramid: { unit: "70% (fast, many tests, mock provider)", integration: "20% (medium, key flows, test backend)", e2e: "10% (slow, critical paths, real UI)" }, // CI/CD Strategy cicd: { on_pr: "Run unit + integration tests", on_merge: "Run all tests (including E2E)", on_deploy: "Run smoke tests (E2E critical paths)" }, // Golden Rule invariant: "Mock providers for unit tests. Real providers for integration. Real UI for E2E." } ``` --- ## Pattern Language ### Pattern: Backend-Agnostic Data Access ```typescript { id: "backend_agnostic_data_access", type: DATA_PATTERN, applicability: (thing) => thing.type === "service" || thing.type === "component", inputs: [DataProviderInterface], outputs: [Data], transform: (operation) => ({ code: ` // Works with ANY backend (Convex, WordPress, Notion, etc.) const provider = yield* DataProvider // Call ontology operation (backend translates) const data = yield* provider.things.get(id) return data ` }), constraints: [ "no_backend_specific_code", "uses_provider_interface", "effect_ts_typed" ], benefits: [ "swap_backends_one_line", "type_safe_errors", "testable_via_mock_provider" ] } ``` ### Pattern: Generic vs Specialized Services ```typescript { id: "generic_vs_specialized_service_decision", type: SERVICE_PATTERN, decision_tree: { // Start with generic question: "Do you need to operate on things?", yes: { default: "Use ThingService (handles all 66 types)", code: ` const thingService = yield* ThingService const course = yield* thingService.get(courseId) const lesson = yield* thingService.get(lessonId) ` }, // Add specialized only if needed question: "Do you repeat the same 2-3 operation sequence 3+ times?", yes: { action: "Create SpecializedService", code: ` // CourseService.ts (OPTIONAL) export class CourseService { getCourseWithLessons: (id) => Effect.all([ thingService.get(id), connectionService.getRelated(id, 'part_of', 'to') ]) } ` }, no: { action: "Keep using generic services", reason: "Don't create specialized services prematurely" } }, constraints: [ "always_have_generic_services", "specialized_services_optional", "add_specialized_when_patterns_emerge" ] } ``` ### Pattern: Provider Swapping ```typescript { id: "provider_swapping", type: CONFIGURATION_PATTERN, applicability: (thing) => thing.type === "configuration", inputs: [ProviderConfig], outputs: [ConfiguredProvider], transform: (config) => ({ // astro.config.ts code: ` import { convexProvider } from './providers/convex' // import { wordpressProvider } from './providers/wordpress' // import { notionProvider } from './providers/notion' export default defineConfig({ integrations: [ one({ // ✅ Change this ONE line to swap backends provider: convexProvider({ url: import.meta.env.PUBLIC_CONVEX_URL }) // OR use WordPress: // provider: wordpressProvider({ // url: 'https://yoursite.com', // apiKey: import.meta.env.WORDPRESS_API_KEY // }) }) ] }) ` }), constraints: [ "one_line_change", "no_code_changes", "components_unchanged" ] } ``` ### Pattern: Progressive Enhancement ```typescript { id: "progressive_enhancement", type: UI_PATTERN, strategy: "Start SSR, enhance with islands, add real-time last", // Level 1: Base (works without JS) base: { layer: "Astro SSR", code: ` <form action="/api/courses/enroll" method="post"> <input type="hidden" name="courseId" value={courseId} /> <button type="submit">Enroll</button> </form> `, works_without_js: true, accessibility: "Full keyboard navigation", seo: "Search engines can see content" }, // Level 2: Enhanced (adds interactivity with JS) enhanced: { layer: "React island", code: ` <EnrollButton client:load courseId={courseId} initialEnrolled={enrolled} /> `, requires_js: true, benefit: "Optimistic UI, instant feedback, loading states", fallback: "Form still works via SSR if JS fails" }, // Level 3: Real-time (live updates) realtime: { layer: "WebSocket subscription", code: ` export function EnrollmentCount({ courseId }: Props) { const count = useQuery(api.connections.getCount, { thingId: courseId, relationshipType: 'enrolled_in' }) return <span>{count} enrolled</span> } `, requires_js: true, requires_subscription: true, benefit: "Live count updates as users enroll", fallback: "SSR shows stale count (still functional)" }, strategy: { 1: "Start with SSR (universal baseline)", 2: "Add islands for interactivity (progressive)", 3: "Add real-time only where needed (rare)", rule: "Each layer enhances, never replaces previous layer" } } ``` --- ## Type Sync Automation ### Automatic Frontend/Backend Type Synchronization ```typescript { principle: "Backend schema = single source of truth. Frontend derives types.", pipeline: { step1: { where: "Backend", what: "Define schema (convex/schema.ts)", code: ` // convex/schema.ts import { defineSchema, defineTable } from 'convex/server' import { v } from 'convex/values' export default defineSchema({ things: defineTable({ type: v.string(), name: v.string(), properties: v.any(), status: v.union( v.literal('draft'), v.literal('published'), v.literal('archived') ), createdAt: v.number(), updatedAt: v.number() }) }) ` }, step2: { where: "Backend deployment", what: "Deploy → Auto-generate types", command: "npx convex deploy", generates: "convex/_generated/dataModel.d.ts", code: ` // Auto-generated by Convex export type Thing = { _id: Id<'things'> _creationTime: number type: string name: string properties: any status: 'draft' | 'published' | 'archived' createdAt: number updatedAt: number } ` }, step3: { where: "Frontend", what: "Import generated types", code: ` // src/services/ThingService.ts import type { Id } from '@/convex/_generated/dataModel' import type { Thing } from '@/convex/_generated/dataModel' export class ThingService { get(id: Id<'things'>): Effect<Thing, Error> { // TypeScript knows exact shape of Thing } } ` }, step4: { where: "Development", what: "Hot reload triggers type regeneration", trigger: "Save convex/schema.ts", action: "Convex dev server regenerates types", result: "TypeScript immediately shows errors in frontend" } }, validation: { ci: { check: "TypeScript compilation", command: "npx astro check", fails_if: "Frontend imports types that don't match backend schema", result: "Can't merge PR if types mismatch" }, dev: { watch: "Convex dev server watches schema changes", regenerates: "Types on every schema save", editor: "VS Code shows TypeScript errors instantly" }, deploy: { blocks: "Can't deploy frontend if types don't match backend", ensures: "Frontend and backend always in sync" } }, benefits: [ "Single source of truth (backend schema)", "Zero manual type definition duplication", "Immediate feedback on type mismatches", "Impossible to deploy mismatched types", "Refactoring is type-safe (rename propagates)" ], example_refactor: ` // Backend: Rename field // convex/schema.ts things: defineTable({ // name: v.string(), // OLD title: v.string(), // NEW