From db0ff1cd9f0a0bb61671819013764735b20f928d Mon Sep 17 00:00:00 2001 From: Adrian Besleaga Date: Sat, 15 Aug 2026 08:59:42 +0200 Subject: [PATCH 1/6] Added examples --- .gitignore | 22 + examples/nodejs-react-typescript/README.md | 115 + .../backend/features/completeTask.ts | 15 + .../backend/features/createTask.ts | 18 + .../backend/features/deleteTask.ts | 12 + .../backend/features/listTasks.ts | 6 + .../backend/features/reopenTask.ts | 15 + .../backend/runtime/aifaRuntime.ts | 42 + .../backend/runtime/feature.ts | 49 + .../nodejs-react-typescript/backend/server.ts | 70 + .../backend/storage/fileTaskStore.ts | 24 + .../backend/storage/memoryTaskStore.ts | 12 + .../nodejs-react-typescript/backend/types.ts | 40 + .../examples/tickets/create-task.md | 40 + .../examples/tickets/ui-slots.md | 34 + .../frontend/index.html | 12 + .../frontend/src/api.ts | 43 + .../frontend/src/components/TaskRow.tsx | 37 + .../contributions/registerContributions.tsx | 98 + .../frontend/src/main.tsx | 176 ++ .../frontend/src/slots/Slot.tsx | 18 + .../frontend/src/slots/slotRegistry.ts | 30 + .../frontend/src/styles.css | 423 ++++ .../frontend/src/types.ts | 56 + .../frontend/src/vite-env.d.ts | 1 + .../nodejs-react-typescript/package-lock.json | 2254 +++++++++++++++++ examples/nodejs-react-typescript/package.json | 30 + .../test/slotRegistry.test.js | 45 + .../test/taskFeatures.test.js | 115 + .../nodejs-react-typescript/tsconfig.json | 20 + examples/vanilla-js/README.md | 79 + .../backend/features/completeTask.js | 45 + .../vanilla-js/backend/features/createTask.js | 40 + .../vanilla-js/backend/features/deleteTask.js | 37 + .../vanilla-js/backend/features/listTasks.js | 18 + .../vanilla-js/backend/features/reopenTask.js | 45 + .../vanilla-js/backend/runtime/aifaRuntime.js | 78 + .../vanilla-js/backend/runtime/feature.js | 38 + examples/vanilla-js/backend/server.js | 136 + .../backend/storage/fileTaskStore.js | 50 + .../backend/storage/memoryTaskStore.js | 24 + .../examples/tickets/complete-task.md | 38 + .../examples/tickets/create-task.md | 37 + .../examples/tickets/delete-task.md | 34 + .../examples/tickets/reopen-task.md | 37 + examples/vanilla-js/frontend/app.js | 130 + examples/vanilla-js/frontend/index.html | 63 + examples/vanilla-js/frontend/styles.css | 280 ++ examples/vanilla-js/package.json | 15 + examples/vanilla-js/test/taskFeatures.test.js | 130 + 50 files changed, 5226 insertions(+) create mode 100644 .gitignore create mode 100644 examples/nodejs-react-typescript/README.md create mode 100644 examples/nodejs-react-typescript/backend/features/completeTask.ts create mode 100644 examples/nodejs-react-typescript/backend/features/createTask.ts create mode 100644 examples/nodejs-react-typescript/backend/features/deleteTask.ts create mode 100644 examples/nodejs-react-typescript/backend/features/listTasks.ts create mode 100644 examples/nodejs-react-typescript/backend/features/reopenTask.ts create mode 100644 examples/nodejs-react-typescript/backend/runtime/aifaRuntime.ts create mode 100644 examples/nodejs-react-typescript/backend/runtime/feature.ts create mode 100644 examples/nodejs-react-typescript/backend/server.ts create mode 100644 examples/nodejs-react-typescript/backend/storage/fileTaskStore.ts create mode 100644 examples/nodejs-react-typescript/backend/storage/memoryTaskStore.ts create mode 100644 examples/nodejs-react-typescript/backend/types.ts create mode 100644 examples/nodejs-react-typescript/examples/tickets/create-task.md create mode 100644 examples/nodejs-react-typescript/examples/tickets/ui-slots.md create mode 100644 examples/nodejs-react-typescript/frontend/index.html create mode 100644 examples/nodejs-react-typescript/frontend/src/api.ts create mode 100644 examples/nodejs-react-typescript/frontend/src/components/TaskRow.tsx create mode 100644 examples/nodejs-react-typescript/frontend/src/contributions/registerContributions.tsx create mode 100644 examples/nodejs-react-typescript/frontend/src/main.tsx create mode 100644 examples/nodejs-react-typescript/frontend/src/slots/Slot.tsx create mode 100644 examples/nodejs-react-typescript/frontend/src/slots/slotRegistry.ts create mode 100644 examples/nodejs-react-typescript/frontend/src/styles.css create mode 100644 examples/nodejs-react-typescript/frontend/src/types.ts create mode 100644 examples/nodejs-react-typescript/frontend/src/vite-env.d.ts create mode 100644 examples/nodejs-react-typescript/package-lock.json create mode 100644 examples/nodejs-react-typescript/package.json create mode 100644 examples/nodejs-react-typescript/test/slotRegistry.test.js create mode 100644 examples/nodejs-react-typescript/test/taskFeatures.test.js create mode 100644 examples/nodejs-react-typescript/tsconfig.json create mode 100644 examples/vanilla-js/README.md create mode 100644 examples/vanilla-js/backend/features/completeTask.js create mode 100644 examples/vanilla-js/backend/features/createTask.js create mode 100644 examples/vanilla-js/backend/features/deleteTask.js create mode 100644 examples/vanilla-js/backend/features/listTasks.js create mode 100644 examples/vanilla-js/backend/features/reopenTask.js create mode 100644 examples/vanilla-js/backend/runtime/aifaRuntime.js create mode 100644 examples/vanilla-js/backend/runtime/feature.js create mode 100644 examples/vanilla-js/backend/server.js create mode 100644 examples/vanilla-js/backend/storage/fileTaskStore.js create mode 100644 examples/vanilla-js/backend/storage/memoryTaskStore.js create mode 100644 examples/vanilla-js/examples/tickets/complete-task.md create mode 100644 examples/vanilla-js/examples/tickets/create-task.md create mode 100644 examples/vanilla-js/examples/tickets/delete-task.md create mode 100644 examples/vanilla-js/examples/tickets/reopen-task.md create mode 100644 examples/vanilla-js/frontend/app.js create mode 100644 examples/vanilla-js/frontend/index.html create mode 100644 examples/vanilla-js/frontend/styles.css create mode 100644 examples/vanilla-js/package.json create mode 100644 examples/vanilla-js/test/taskFeatures.test.js diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..bbc1822 --- /dev/null +++ b/.gitignore @@ -0,0 +1,22 @@ +# Dependencies +node_modules/ + +# Generated application bundles +dist/ + +# Local runtime data for the React + Node example +examples/aifa-react-node-slots/data/tasks.json + +# Test coverage +coverage/ + +# Environment-specific configuration +.env +.env.* +!.env.example + +# OS and editor files +.DS_Store +.idea/ +.vscode/ +*.swp diff --git a/examples/nodejs-react-typescript/README.md b/examples/nodejs-react-typescript/README.md new file mode 100644 index 0000000..d15aa31 --- /dev/null +++ b/examples/nodejs-react-typescript/README.md @@ -0,0 +1,115 @@ +# AIFA React + Node Slot App + +This is a full-stack example app for **AI Atomic Feature Architecture**. + +It includes: + +- Node.js backend +- TypeScript React frontend +- AIFA atomic backend features +- file-backed persistence +- behavioral tests + +## Why Slots? + +Named slots as extension points. A slot is placed in the UI, and independent contributions can render content into that slot with an order and a model. + +This example borrows the concept for React: + +```jsx + +``` + +Then another module contributes UI: + +```jsx +registerSlotContribution({ + slot: "TASK_ACTIONS", + name: "complete-task-action", + order: 10, + render: ({ model }) => , +}); +``` + +This keeps the frontend extensible in the same spirit as AIFA: + +- backend features are atomic +- frontend UI areas are explicit extension points +- contributions are small, ordered, and isolated + +## Run + +Install dependencies once: + +```sh +npm install +``` + +Start the API: + +```sh +npm run api +``` + +Start the React app in another terminal: + +```sh +npm run dev +``` + +The frontend is type-checked as part of `npm run build`. + +Open: + +```txt +http://127.0.0.1:5173 +``` + +## Test + +```sh +npm test +``` + +## Architecture + +```txt +React UI + | + | renders Slot(name, model) + v +Slot Registry + | + | ordered UI contributions + v +Task Board UI + +HTTP API + | + | runs one feature per route + v +AIFA Runtime + | + | exposes declared capabilities + v +File-backed storage +``` + +## Atomic Backend Features + +| Feature | Route | Purpose | +| --- | --- | --- | +| `create-task` | `POST /api/tasks` | Create one task | +| `list-tasks` | `GET /api/tasks` | List tasks | +| `complete-task` | `POST /api/tasks/:id/complete` | Complete one task | +| `reopen-task` | `POST /api/tasks/:id/reopen` | Reopen one task | +| `delete-task` | `DELETE /api/tasks/:id` | Delete one task | + +## Frontend Slots + +| Slot | Model | Purpose | +| --- | --- | --- | +| `APP_HEADER_ACTIONS` | `{ tasks, refreshTasks }` | Add header-level actions | +| `TASK_SUMMARY_CARDS` | `{ tasks }` | Add dashboard summary cards | +| `TASK_ACTIONS` | `{ task, completeTask, reopenTask, deleteTask }` | Add row-level task actions | +| `EMPTY_STATE_ACTIONS` | `{ createExampleTask }` | Add empty state actions | diff --git a/examples/nodejs-react-typescript/backend/features/completeTask.ts b/examples/nodejs-react-typescript/backend/features/completeTask.ts new file mode 100644 index 0000000..34fea50 --- /dev/null +++ b/examples/nodejs-react-typescript/backend/features/completeTask.ts @@ -0,0 +1,15 @@ +import { defineFeature } from "../runtime/feature"; +import type { Task } from "../types"; +interface TaskIdInput { taskId: string; } +export const completeTask = defineFeature({ + name: "complete-task", description: "Mark one existing task as completed.", input: { taskId: "string" }, output: { task: "Task" }, capabilities: ["clock.now", "task.load", "task.save", "audit.record"], + async execute(context) { + const task = await context.capabilities["task.load"]({ taskId: context.input.taskId }); + if (!task) return context.fail("not_found", "Task was not found", { taskId: context.input.taskId }); + if (task.completed) return context.ok({ task }); + const completedTask: Task = { ...task, completed: true, completedAt: await context.capabilities["clock.now"]() }; + await context.capabilities["task.save"]({ task: completedTask }); + await context.capabilities["audit.record"]({ type: "task.completed", actorId: context.actor.id, taskId: task.id }); + return context.ok({ task: completedTask }); + }, +}); diff --git a/examples/nodejs-react-typescript/backend/features/createTask.ts b/examples/nodejs-react-typescript/backend/features/createTask.ts new file mode 100644 index 0000000..528fa18 --- /dev/null +++ b/examples/nodejs-react-typescript/backend/features/createTask.ts @@ -0,0 +1,18 @@ +import { defineFeature } from "../runtime/feature"; +import type { Priority, Task } from "../types"; + +interface CreateTaskInput { title?: string; priority?: string; } +export const createTask = defineFeature({ + name: "create-task", description: "Create one task with a non-empty title and optional priority.", + input: { title: "string", priority: "low | medium | high" }, output: { task: "Task" }, + capabilities: ["id.create", "clock.now", "task.create", "audit.record"], + async execute(context) { + const title = context.input.title?.trim() ?? ""; + if (!title) return context.fail("invalid_title", "Task title is required"); + const task: Task = { id: await context.capabilities["id.create"](), title, priority: normalizePriority(context.input.priority), completed: false, createdAt: await context.capabilities["clock.now"](), completedAt: null }; + await context.capabilities["task.create"]({ task }); + await context.capabilities["audit.record"]({ type: "task.created", actorId: context.actor.id, taskId: task.id }); + return context.ok({ task }); + }, +}); +function normalizePriority(priority: string | undefined): Priority { return priority === "low" || priority === "medium" || priority === "high" ? priority : "medium"; } diff --git a/examples/nodejs-react-typescript/backend/features/deleteTask.ts b/examples/nodejs-react-typescript/backend/features/deleteTask.ts new file mode 100644 index 0000000..b9b1e96 --- /dev/null +++ b/examples/nodejs-react-typescript/backend/features/deleteTask.ts @@ -0,0 +1,12 @@ +import { defineFeature } from "../runtime/feature"; +interface TaskIdInput { taskId: string; } +export const deleteTask = defineFeature({ + name: "delete-task", description: "Delete one existing task.", input: { taskId: "string" }, output: { deleted: "boolean" }, capabilities: ["task.load", "task.delete", "audit.record"], + async execute(context) { + const task = await context.capabilities["task.load"]({ taskId: context.input.taskId }); + if (!task) return context.fail("not_found", "Task was not found", { taskId: context.input.taskId }); + await context.capabilities["task.delete"]({ taskId: task.id }); + await context.capabilities["audit.record"]({ type: "task.deleted", actorId: context.actor.id, taskId: task.id }); + return context.ok({ deleted: true }); + }, +}); diff --git a/examples/nodejs-react-typescript/backend/features/listTasks.ts b/examples/nodejs-react-typescript/backend/features/listTasks.ts new file mode 100644 index 0000000..e051b7b --- /dev/null +++ b/examples/nodejs-react-typescript/backend/features/listTasks.ts @@ -0,0 +1,6 @@ +import { defineFeature } from "../runtime/feature"; +import type { Task } from "../types"; +export const listTasks = defineFeature, { tasks: Task[] }>({ + name: "list-tasks", description: "List tasks in creation order.", input: {}, output: { tasks: "Task[]" }, capabilities: ["task.list"], + async execute(context) { return context.ok({ tasks: await context.capabilities["task.list"]() }); }, +}); diff --git a/examples/nodejs-react-typescript/backend/features/reopenTask.ts b/examples/nodejs-react-typescript/backend/features/reopenTask.ts new file mode 100644 index 0000000..e71563f --- /dev/null +++ b/examples/nodejs-react-typescript/backend/features/reopenTask.ts @@ -0,0 +1,15 @@ +import { defineFeature } from "../runtime/feature"; +import type { Task } from "../types"; +interface TaskIdInput { taskId: string; } +export const reopenTask = defineFeature({ + name: "reopen-task", description: "Mark one completed task as active again.", input: { taskId: "string" }, output: { task: "Task" }, capabilities: ["task.load", "task.save", "audit.record"], + async execute(context) { + const task = await context.capabilities["task.load"]({ taskId: context.input.taskId }); + if (!task) return context.fail("not_found", "Task was not found", { taskId: context.input.taskId }); + if (!task.completed) return context.ok({ task }); + const reopenedTask: Task = { ...task, completed: false, completedAt: null }; + await context.capabilities["task.save"]({ task: reopenedTask }); + await context.capabilities["audit.record"]({ type: "task.reopened", actorId: context.actor.id, taskId: task.id }); + return context.ok({ task: reopenedTask }); + }, +}); diff --git a/examples/nodejs-react-typescript/backend/runtime/aifaRuntime.ts b/examples/nodejs-react-typescript/backend/runtime/aifaRuntime.ts new file mode 100644 index 0000000..b01f0d5 --- /dev/null +++ b/examples/nodejs-react-typescript/backend/runtime/aifaRuntime.ts @@ -0,0 +1,42 @@ +import { fail, ok, type Feature, type FeatureResult } from "./feature"; +import type { Actor, AuditEntry, RuntimeCapabilities, TaskStore } from "../types"; + +interface IdFactory { next(): string; } +interface RuntimeOptions { taskStore: TaskStore; idFactory?: IdFactory; clock?: () => Date; } + +export function createAifaRuntime({ taskStore, idFactory = createIdFactory(), clock = () => new Date() }: RuntimeOptions) { + const auditLog: AuditEntry[] = []; + const allCapabilities: RuntimeCapabilities = { + "id.create": async () => idFactory.next(), + "clock.now": async () => clock().toISOString(), + "task.create": async ({ task }) => taskStore.create(task), + "task.load": async ({ taskId }) => taskStore.load(taskId), + "task.save": async ({ task }) => taskStore.save(task), + "task.delete": async ({ taskId }) => taskStore.delete(taskId), + "task.list": async () => taskStore.list(), + "audit.record": async (entry) => { auditLog.push({ ...entry, recordedAt: clock().toISOString() }); }, + }; + + function bindCapabilities(feature: Feature): RuntimeCapabilities { + const allowed = new Set(feature.capabilities); + return new Proxy(allCapabilities, { + get(target, capabilityName) { + if (typeof capabilityName !== "string") return undefined; + if (!allowed.has(capabilityName as keyof RuntimeCapabilities)) { + return async () => fail("capability_not_allowed", `Feature '${feature.name}' cannot use '${capabilityName}'`); + } + return target[capabilityName as keyof RuntimeCapabilities] ?? (async () => fail("capability_missing", `Runtime does not provide '${capabilityName}'`)); + }, + }); + } + + async function run(feature: Feature, input: Input, actor: Actor = { id: "demo-user" }, metadata: Record = {}): Promise> { + return feature.execute({ input, actor, metadata, capabilities: bindCapabilities(feature), ok, fail }); + } + + return { auditLog, run }; +} + +function createIdFactory(): IdFactory { + return { next: () => globalThis.crypto?.randomUUID?.() ?? `task-${Date.now()}-${Math.random().toString(16).slice(2)}` }; +} diff --git a/examples/nodejs-react-typescript/backend/runtime/feature.ts b/examples/nodejs-react-typescript/backend/runtime/feature.ts new file mode 100644 index 0000000..cbb9989 --- /dev/null +++ b/examples/nodejs-react-typescript/backend/runtime/feature.ts @@ -0,0 +1,49 @@ +import type { Actor, RuntimeCapabilities } from "../types"; + +export interface FeatureSuccess { + ok: true; + value: Value; +} + +export interface FeatureFailure { + ok: false; + error: { code: string; message: string; details: Record }; +} + +export type FeatureResult = FeatureSuccess | FeatureFailure; + +export interface FeatureContext { + input: Input; + actor: Actor; + metadata: Record; + capabilities: RuntimeCapabilities; + ok: (value: Value) => FeatureSuccess; + fail: (code: string, message: string, details?: Record) => FeatureFailure; +} + +export interface Feature { + name: string; + description: string; + input: Record; + output: Record; + capabilities: (keyof RuntimeCapabilities)[]; + execute(context: FeatureContext): Promise>; +} + +export function defineFeature(definition: Feature): Readonly> { + const requiredFields = ["name", "description", "input", "output", "capabilities", "execute"]; + for (const field of requiredFields) { + if (!(field in definition)) throw new Error(`Feature is missing required field: ${field}`); + } + if (!Array.isArray(definition.capabilities)) throw new Error("Feature capabilities must be an array"); + if (typeof definition.execute !== "function") throw new Error("Feature execute must be a function"); + return Object.freeze({ ...definition }); +} + +export function ok(value: Value): FeatureSuccess { + return { ok: true, value }; +} + +export function fail(code: string, message: string, details: Record = {}): FeatureFailure { + return { ok: false, error: { code, message, details } }; +} diff --git a/examples/nodejs-react-typescript/backend/server.ts b/examples/nodejs-react-typescript/backend/server.ts new file mode 100644 index 0000000..0817820 --- /dev/null +++ b/examples/nodejs-react-typescript/backend/server.ts @@ -0,0 +1,70 @@ +import { createServer, type IncomingMessage, type ServerResponse } from "node:http"; +import { readFile } from "node:fs/promises"; +import { extname, join } from "node:path"; +import { fileURLToPath } from "node:url"; + +import { completeTask } from "./features/completeTask"; +import { createTask } from "./features/createTask"; +import { deleteTask } from "./features/deleteTask"; +import { listTasks } from "./features/listTasks"; +import { reopenTask } from "./features/reopenTask"; +import { createAifaRuntime } from "./runtime/aifaRuntime"; +import { createFileTaskStore } from "./storage/fileTaskStore"; +import type { FeatureResult } from "./runtime/feature"; + +const rootDir = join(fileURLToPath(new URL(".", import.meta.url)), ".."); +const dataPath = join(rootDir, "data", "tasks.json"); +const distDir = join(rootDir, "dist"); +const contentTypes: Record = { ".html": "text/html; charset=utf-8", ".css": "text/css; charset=utf-8", ".js": "text/javascript; charset=utf-8", ".json": "application/json; charset=utf-8", ".svg": "image/svg+xml" }; +const runtime = createAifaRuntime({ taskStore: createFileTaskStore(dataPath) }); + +const server = createServer(async (request, response) => { + try { + writeCorsHeaders(response); + if (request.method === "OPTIONS") { response.writeHead(204); response.end(); return; } + const url = new URL(request.url ?? "/", `http://${request.headers.host ?? "127.0.0.1"}`); + if (url.pathname.startsWith("/api/")) { await handleApi(request, response, url); return; } + await serveStatic(response, url.pathname); + } catch (error: unknown) { + sendJson(response, 500, { ok: false, error: { code: "server_error", message: error instanceof Error ? error.message : "Unexpected server error" } }); + } +}); + +async function handleApi(request: IncomingMessage, response: ServerResponse, url: URL): Promise { + const demoUser = request.headers["x-demo-user"]; + const actor = { id: typeof demoUser === "string" ? demoUser : "demo-user" }; + if (request.method === "GET" && url.pathname === "/api/tasks") { sendFeatureResult(response, await runtime.run(listTasks, {}, actor)); return; } + if (request.method === "POST" && url.pathname === "/api/tasks") { sendFeatureResult(response, await runtime.run(createTask, await readJson(request), actor), 201); return; } + const actionMatch = url.pathname.match(/^\/api\/tasks\/([^/]+)\/(complete|reopen)$/); + if (request.method === "POST" && actionMatch) { + const [, taskId, action] = actionMatch; + sendFeatureResult(response, action === "complete" ? await runtime.run(completeTask, { taskId }, actor) : await runtime.run(reopenTask, { taskId }, actor)); + return; + } + const deleteMatch = url.pathname.match(/^\/api\/tasks\/([^/]+)$/); + if (request.method === "DELETE" && deleteMatch) { sendFeatureResult(response, await runtime.run(deleteTask, { taskId: deleteMatch[1] }, actor)); return; } + sendJson(response, 404, { ok: false, error: { code: "route_not_found", message: "API route was not found" } }); +} + +async function serveStatic(response: ServerResponse, pathname: string): Promise { + const requestedFile = pathname === "/" ? "index.html" : pathname.slice(1); + const filePath = join(distDir, requestedFile); + response.writeHead(200, { "content-type": contentTypes[extname(filePath)] ?? "application/octet-stream" }); + response.end(await readFile(filePath)); +} + +async function readJson(request: IncomingMessage): Promise<{ title?: string; priority?: string }> { + const chunks: Buffer[] = []; + for await (const chunk of request) chunks.push(Buffer.from(chunk)); + return chunks.length === 0 ? {} : JSON.parse(Buffer.concat(chunks).toString("utf8")) as { title?: string; priority?: string }; +} + +function sendFeatureResult(response: ServerResponse, result: FeatureResult, successStatus = 200): void { + sendJson(response, result.ok ? successStatus : result.error.code === "not_found" ? 404 : 400, result); +} +function sendJson(response: ServerResponse, status: number, payload: unknown): void { response.writeHead(status, { "content-type": "application/json; charset=utf-8" }); response.end(JSON.stringify(payload, null, 2)); } +function writeCorsHeaders(response: ServerResponse): void { response.setHeader("access-control-allow-origin", "*"); response.setHeader("access-control-allow-methods", "GET,POST,DELETE,OPTIONS"); response.setHeader("access-control-allow-headers", "content-type,x-demo-user"); } + +const port = Number(process.env.PORT ?? 3000); +const host = process.env.HOST ?? "127.0.0.1"; +server.listen(port, host, () => console.log(`AIFA React Slot App running at http://${host}:${port}`)); diff --git a/examples/nodejs-react-typescript/backend/storage/fileTaskStore.ts b/examples/nodejs-react-typescript/backend/storage/fileTaskStore.ts new file mode 100644 index 0000000..d571e26 --- /dev/null +++ b/examples/nodejs-react-typescript/backend/storage/fileTaskStore.ts @@ -0,0 +1,24 @@ +import { mkdir, readFile, writeFile } from "node:fs/promises"; +import { dirname } from "node:path"; +import type { Task, TaskStore } from "../types"; + +export function createFileTaskStore(filePath: string): TaskStore { + async function readTasks(): Promise { + try { return JSON.parse(await readFile(filePath, "utf8")) as Task[]; } + catch (error: unknown) { + if (error instanceof Error && "code" in error && error.code === "ENOENT") return []; + throw error; + } + } + async function writeTasks(tasks: Task[]): Promise { + await mkdir(dirname(filePath), { recursive: true }); + await writeFile(filePath, JSON.stringify(tasks, null, 2)); + } + return { + async create(task) { const tasks = await readTasks(); await writeTasks([...tasks, task]); return task; }, + async load(taskId) { return (await readTasks()).find((task) => task.id === taskId) ?? null; }, + async save(task) { const tasks = await readTasks(); await writeTasks(tasks.map((stored) => stored.id === task.id ? task : stored)); return task; }, + async delete(taskId) { const tasks = await readTasks(); const next = tasks.filter((task) => task.id !== taskId); await writeTasks(next); return next.length !== tasks.length; }, + async list() { return (await readTasks()).sort((left, right) => left.createdAt.localeCompare(right.createdAt)); }, + }; +} diff --git a/examples/nodejs-react-typescript/backend/storage/memoryTaskStore.ts b/examples/nodejs-react-typescript/backend/storage/memoryTaskStore.ts new file mode 100644 index 0000000..910fbdc --- /dev/null +++ b/examples/nodejs-react-typescript/backend/storage/memoryTaskStore.ts @@ -0,0 +1,12 @@ +import type { Task, TaskStore } from "../types"; + +export function createMemoryTaskStore(seed: Task[] = []): TaskStore { + const tasks = new Map(seed.map((task) => [task.id, task])); + return { + async create(task) { tasks.set(task.id, task); return task; }, + async load(taskId) { return tasks.get(taskId) ?? null; }, + async save(task) { tasks.set(task.id, task); return task; }, + async delete(taskId) { return tasks.delete(taskId); }, + async list() { return [...tasks.values()].sort((left, right) => left.createdAt.localeCompare(right.createdAt)); }, + }; +} diff --git a/examples/nodejs-react-typescript/backend/types.ts b/examples/nodejs-react-typescript/backend/types.ts new file mode 100644 index 0000000..67ed34b --- /dev/null +++ b/examples/nodejs-react-typescript/backend/types.ts @@ -0,0 +1,40 @@ +export type Priority = "low" | "medium" | "high"; + +export interface Task { + id: string; + title: string; + priority: Priority; + completed: boolean; + createdAt: string; + completedAt: string | null; +} + +export interface Actor { + id: string; +} + +export interface AuditEntry { + type: string; + actorId: string; + taskId: string; + recordedAt?: string; +} + +export interface TaskStore { + create(task: Task): Promise; + load(taskId: string): Promise; + save(task: Task): Promise; + delete(taskId: string): Promise; + list(): Promise; +} + +export interface RuntimeCapabilities { + "id.create": () => Promise; + "clock.now": () => Promise; + "task.create": (input: { task: Task }) => Promise; + "task.load": (input: { taskId: string }) => Promise; + "task.save": (input: { task: Task }) => Promise; + "task.delete": (input: { taskId: string }) => Promise; + "task.list": () => Promise; + "audit.record": (entry: AuditEntry) => Promise; +} diff --git a/examples/nodejs-react-typescript/examples/tickets/create-task.md b/examples/nodejs-react-typescript/examples/tickets/create-task.md new file mode 100644 index 0000000..18eeab6 --- /dev/null +++ b/examples/nodejs-react-typescript/examples/tickets/create-task.md @@ -0,0 +1,40 @@ +# Feature: Create Task + +## Goal + +Allow an actor to create one active task from the React UI. + +## Input + +- `title`: string - The task title. +- `priority`: `low | medium | high` - Optional task priority. + +## Output + +- `task`: Task - The created task. + +## Allowed Capabilities + +- `id.create` - Generate a task id. +- `clock.now` - Timestamp the task. +- `task.create` - Persist the task. +- `audit.record` - Record the creation event. + +## Rules + +- The title must not be empty after trimming. +- Missing or invalid priority becomes `medium`. +- New tasks start as incomplete. +- New tasks have no completion timestamp. + +## Failure Cases + +- `invalid_title` - The title is empty. + +## Acceptance Tests + +- Creates an active task. +- Trims the title. +- Defaults unknown priority to `medium`. +- Rejects empty titles. + diff --git a/examples/nodejs-react-typescript/examples/tickets/ui-slots.md b/examples/nodejs-react-typescript/examples/tickets/ui-slots.md new file mode 100644 index 0000000..1cf3612 --- /dev/null +++ b/examples/nodejs-react-typescript/examples/tickets/ui-slots.md @@ -0,0 +1,34 @@ +# Frontend Extension: Task Board Slots + +## Goal + +Allow the React UI to expose named extension points that independent contributions can render into. + +## Slot Contract + +Each contribution has: + +- `slot`: string - The target UI slot. +- `name`: string - Unique contribution name within the slot. +- `order`: number - Sort order. +- `render`: function - Receives `{ model, contribution }` and returns React UI. + +## Slots + +- `APP_HEADER_ACTIONS` - Header actions. +- `TASK_SUMMARY_CARDS` - Dashboard cards. +- `TASK_ACTIONS` - Per-task row actions. +- `EMPTY_STATE_ACTIONS` - Empty state actions. + +## Rules + +- Contributions are sorted by `order`. +- Contributions with the same name replace the previous contribution. +- Slot models provide only the data/actions needed by that slot. + +## Why This Matters + +The application owns stable extension points, while add-ons contribute UI independently. + +In AIFA terms, it makes frontend customization atomic too. + diff --git a/examples/nodejs-react-typescript/frontend/index.html b/examples/nodejs-react-typescript/frontend/index.html new file mode 100644 index 0000000..fa1bd13 --- /dev/null +++ b/examples/nodejs-react-typescript/frontend/index.html @@ -0,0 +1,12 @@ + + + + + + AIFA React Slot App + + +
+ + + diff --git a/examples/nodejs-react-typescript/frontend/src/api.ts b/examples/nodejs-react-typescript/frontend/src/api.ts new file mode 100644 index 0000000..41dc996 --- /dev/null +++ b/examples/nodejs-react-typescript/frontend/src/api.ts @@ -0,0 +1,43 @@ +import type { ApiResult, ApiSuccess, Task } from "./types"; + +const apiBaseUrl = import.meta.env.VITE_API_BASE_URL ?? "http://127.0.0.1:3000"; + +export function listTasks(): Promise> { + return request("/api/tasks"); +} + +export function createTask(task: Pick): Promise> { + return request("/api/tasks", { + method: "POST", + body: JSON.stringify(task), + }); +} + +export function setTaskCompleted(taskId: string, completed: boolean): Promise> { + return request(`/api/tasks/${taskId}/${completed ? "complete" : "reopen"}`, { + method: "POST", + }); +} + +export function deleteTask(taskId: string): Promise> { + return request(`/api/tasks/${taskId}`, { + method: "DELETE", + }); +} + +async function request(path: string, options: RequestInit = {}): Promise> { + const response = await fetch(`${apiBaseUrl}${path}`, { + headers: { + "content-type": "application/json", + ...options.headers, + }, + ...options, + }); + const result = (await response.json()) as ApiResult; + + if (!response.ok || !result.ok) { + throw new Error(result.ok ? "Request failed" : result.error.message); + } + + return result; +} diff --git a/examples/nodejs-react-typescript/frontend/src/components/TaskRow.tsx b/examples/nodejs-react-typescript/frontend/src/components/TaskRow.tsx new file mode 100644 index 0000000..a868248 --- /dev/null +++ b/examples/nodejs-react-typescript/frontend/src/components/TaskRow.tsx @@ -0,0 +1,37 @@ +import { Slot } from "../slots/Slot"; + +import type { Task } from "../types"; + +interface TaskRowProperties { + task: Task; + onComplete: (task: Task, completed: boolean) => Promise; + onDelete: (task: Task) => Promise; +} + +export function TaskRow({ task, onComplete, onDelete }: TaskRowProperties) { + return ( +
  • +
  • + ); +} diff --git a/examples/nodejs-react-typescript/frontend/src/contributions/registerContributions.tsx b/examples/nodejs-react-typescript/frontend/src/contributions/registerContributions.tsx new file mode 100644 index 0000000..6f147d1 --- /dev/null +++ b/examples/nodejs-react-typescript/frontend/src/contributions/registerContributions.tsx @@ -0,0 +1,98 @@ +import { registerSlotContribution } from "../slots/slotRegistry"; + +registerSlotContribution({ + slot: "APP_HEADER_ACTIONS", + name: "refresh-button", + order: 10, + render: ({ model }) => ( + + ), +}); + +registerSlotContribution({ + slot: "TASK_SUMMARY_CARDS", + name: "active-task-count", + order: 10, + render: ({ model }) => { + const activeCount = model.tasks.filter((task) => !task.completed).length; + + return ( +
    + Active + {activeCount} +
    + ); + }, +}); + +registerSlotContribution({ + slot: "TASK_SUMMARY_CARDS", + name: "completed-task-count", + order: 20, + render: ({ model }) => { + const completedCount = model.tasks.filter((task) => task.completed).length; + + return ( +
    + Done + {completedCount} +
    + ); + }, +}); + +registerSlotContribution({ + slot: "TASK_SUMMARY_CARDS", + name: "high-priority-count", + order: 30, + render: ({ model }) => { + const highPriorityCount = model.tasks.filter((task) => task.priority === "high" && !task.completed).length; + + return ( +
    + High priority + {highPriorityCount} +
    + ); + }, +}); + +registerSlotContribution({ + slot: "TASK_ACTIONS", + name: "completion-action", + order: 10, + render: ({ model }) => + model.task.completed ? ( + + ) : ( + + ), +}); + +registerSlotContribution({ + slot: "TASK_ACTIONS", + name: "delete-action", + order: 20, + render: ({ model }) => ( + + ), +}); + +registerSlotContribution({ + slot: "EMPTY_STATE_ACTIONS", + name: "create-example-task", + order: 10, + render: ({ model }) => ( + + ), +}); diff --git a/examples/nodejs-react-typescript/frontend/src/main.tsx b/examples/nodejs-react-typescript/frontend/src/main.tsx new file mode 100644 index 0000000..2457ea2 --- /dev/null +++ b/examples/nodejs-react-typescript/frontend/src/main.tsx @@ -0,0 +1,176 @@ +import { useEffect, useMemo, useState, type FormEvent } from "react"; +import { createRoot } from "react-dom/client"; + +import { createTask, deleteTask, listTasks, setTaskCompleted } from "./api"; +import { TaskRow } from "./components/TaskRow"; +import "./contributions/registerContributions"; +import { Slot } from "./slots/Slot"; +import "./styles.css"; + +import type { AppSlotModel, Priority, Task } from "./types"; + +function App() { + const [tasks, setTasks] = useState([]); + const [title, setTitle] = useState(""); + const [priority, setPriority] = useState("medium"); + const [filter, setFilter] = useState<"all" | "active" | "completed">("all"); + const [activeFeature, setActiveFeature] = useState("list-tasks"); + const [status, setStatus] = useState("Runtime ready"); + + async function refreshTasks(featureName = "list-tasks"): Promise { + const result = await listTasks(); + setTasks(result.value.tasks); + setActiveFeature(featureName); + setStatus(`${result.value.tasks.length} task${result.value.tasks.length === 1 ? "" : "s"}`); + } + + useEffect(() => { + void refreshTasks().catch((error: unknown) => setStatus(error instanceof Error ? error.message : "Request failed")); + }, []); + + const visibleTasks = useMemo(() => { + if (filter === "active") { + return tasks.filter((task) => !task.completed); + } + + if (filter === "completed") { + return tasks.filter((task) => task.completed); + } + + return tasks; + }, [filter, tasks]); + + async function handleCreateTask(event: FormEvent): Promise { + event.preventDefault(); + + if (!title.trim()) { + setStatus("Title required"); + return; + } + + await createTask({ title, priority }); + setTitle(""); + await refreshTasks("create-task"); + } + + async function handleTaskCompletion(task: Task, completed: boolean): Promise { + await setTaskCompleted(task.id, completed); + await refreshTasks(completed ? "complete-task" : "reopen-task"); + } + + async function handleDeleteTask(task: Task): Promise { + await deleteTask(task.id); + await refreshTasks("delete-task"); + } + + async function createExampleTask(): Promise { + await createTask({ + title: "Implement one atomic feature", + priority: "high", + }); + await refreshTasks("create-task"); + } + + const appModel: AppSlotModel = { + tasks, + visibleTasks, + activeFeature, + refreshTasks, + createExampleTask, + }; + + return ( +
    +
    +
    +
    +

    React + Node AIFA example

    +

    Atomic Task Board

    +
    +
    + +
    {status}
    +
    +
    + +
    + +
    + +
    void handleCreateTask(event)}> + setTitle(event.target.value)} + placeholder="Add an atomic task..." + aria-label="Task title" + /> + + +
    + +
    + {(["all", "active", "completed"] as const).map((option) => ( + + ))} +
    + +
      + {visibleTasks.map((task) => ( + + ))} +
    + + {visibleTasks.length === 0 && ( +
    +

    No tasks in this view.

    + +
    + )} +
    + + +
    + ); +} + +const rootElement = document.querySelector("#root"); + +if (!rootElement) { + throw new Error("React root element was not found"); +} + +createRoot(rootElement).render(); diff --git a/examples/nodejs-react-typescript/frontend/src/slots/Slot.tsx b/examples/nodejs-react-typescript/frontend/src/slots/Slot.tsx new file mode 100644 index 0000000..f02cb6b --- /dev/null +++ b/examples/nodejs-react-typescript/frontend/src/slots/Slot.tsx @@ -0,0 +1,18 @@ +import { getSlotContributions } from "./slotRegistry"; + +import type { SlotModels, SlotName } from "../types"; + +interface SlotProperties { + name: Name; + model: SlotModels[Name]; +} + +export function Slot({ name, model }: SlotProperties) { + const contributions = getSlotContributions(name); + + return contributions.map((contribution) => ( + + {contribution.render({ model, contribution })} + + )); +} diff --git a/examples/nodejs-react-typescript/frontend/src/slots/slotRegistry.ts b/examples/nodejs-react-typescript/frontend/src/slots/slotRegistry.ts new file mode 100644 index 0000000..796d6dc --- /dev/null +++ b/examples/nodejs-react-typescript/frontend/src/slots/slotRegistry.ts @@ -0,0 +1,30 @@ +import type { ReactNode } from "react"; + +import type { SlotModels, SlotName } from "../types"; + +export interface SlotContribution { + slot: Name; + name: string; + order?: number; + render: (properties: { model: SlotModels[Name]; contribution: SlotContribution }) => ReactNode; +} + +const contributions = new Map(); + +export function registerSlotContribution(contribution: SlotContribution): void { + const normalizedContribution: SlotContribution = { + order: 100, + ...contribution, + }; + const slotContributions = (contributions.get(contribution.slot) ?? []) as SlotContribution[]; + const nextContributions = [ + ...slotContributions.filter((candidate) => candidate.name !== contribution.name), + normalizedContribution, + ].sort((left, right) => (left.order ?? 100) - (right.order ?? 100) || left.name.localeCompare(right.name)); + + contributions.set(contribution.slot, nextContributions); +} + +export function getSlotContributions(slot: Name): SlotContribution[] { + return (contributions.get(slot) ?? []) as SlotContribution[]; +} diff --git a/examples/nodejs-react-typescript/frontend/src/styles.css b/examples/nodejs-react-typescript/frontend/src/styles.css new file mode 100644 index 0000000..18b34ea --- /dev/null +++ b/examples/nodejs-react-typescript/frontend/src/styles.css @@ -0,0 +1,423 @@ +:root { + --ink: #121a17; + --muted: #65746e; + --paper: #f7f7f2; + --panel: #ffffff; + --line: #dce6df; + --mint: #2ec49a; + --teal: #117f8c; + --coral: #f26b5b; + --gold: #e1aa35; +} + +* { + box-sizing: border-box; +} + +body { + margin: 0; + min-height: 100vh; + background: + radial-gradient(circle at 16% 8%, rgba(46, 196, 154, 0.2), transparent 26%), + radial-gradient(circle at 88% 78%, rgba(242, 107, 91, 0.16), transparent 26%), + linear-gradient(135deg, #eff7f2, var(--paper)); + color: var(--ink); + font-family: + Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", + sans-serif; +} + +button, +input, +select { + font: inherit; +} + +button { + cursor: pointer; +} + +.app-shell { + display: grid; + gap: 24px; + grid-template-columns: minmax(0, 1fr) 390px; + min-height: 100vh; + padding: 28px; +} + +.workspace, +.architecture-panel { + background: rgba(255, 255, 255, 0.82); + border: 1px solid var(--line); + border-radius: 8px; + box-shadow: 0 24px 70px rgba(18, 26, 23, 0.1); +} + +.workspace { + padding: clamp(24px, 4vw, 52px); +} + +.workspace-header { + align-items: flex-start; + display: flex; + gap: 18px; + justify-content: space-between; + margin-bottom: 24px; +} + +.header-actions { + align-items: center; + display: flex; + flex-wrap: wrap; + gap: 10px; + justify-content: flex-end; +} + +.eyebrow { + color: var(--teal); + font-size: 12px; + font-weight: 900; + letter-spacing: 0; + margin: 0 0 8px; + text-transform: uppercase; +} + +h1, +h2, +p { + margin-top: 0; +} + +h1 { + font-size: clamp(42px, 7vw, 84px); + letter-spacing: 0; + line-height: 0.95; + margin-bottom: 0; +} + +h2 { + font-size: 31px; + letter-spacing: 0; + line-height: 1.06; + margin-bottom: 20px; +} + +.status-pill, +.icon-button, +.action-button, +.task-form button, +.filter { + border-radius: 8px; + font-weight: 850; + min-height: 40px; +} + +.status-pill { + background: #12221c; + color: #effaf6; + font-size: 14px; + padding: 9px 12px; +} + +.icon-button, +.action-button, +.filter { + border: 1px solid var(--line); + background: var(--panel); + color: var(--ink); + padding: 9px 12px; +} + +.summary-grid { + display: grid; + gap: 12px; + grid-template-columns: repeat(3, minmax(0, 1fr)); + margin-bottom: 18px; +} + +.slot-contribution { + display: contents; +} + +.summary-card { + background: var(--panel); + border: 1px solid var(--line); + border-radius: 8px; + padding: 16px; +} + +.summary-card span { + color: var(--muted); + display: block; + font-size: 13px; + font-weight: 850; + margin-bottom: 8px; +} + +.summary-card strong { + display: block; + font-size: 36px; + line-height: 1; +} + +.summary-card.warning { + border-color: rgba(225, 170, 53, 0.55); + background: #fff8e8; +} + +.task-form { + display: grid; + gap: 12px; + grid-template-columns: minmax(0, 1fr) 150px auto; + margin-bottom: 18px; +} + +.task-form input, +.task-form select { + background: var(--panel); + border: 1px solid var(--line); + border-radius: 8px; + color: var(--ink); + min-height: 54px; + padding: 0 14px; +} + +.task-form input:focus, +.task-form select:focus { + border-color: var(--mint); + outline: 3px solid rgba(46, 196, 154, 0.18); +} + +.task-form button, +.action-button.primary { + background: var(--ink); + border: 0; + color: var(--paper); + padding: 0 22px; +} + +.task-toolbar { + display: flex; + gap: 8px; + margin-bottom: 18px; +} + +.filter { + color: var(--muted); + text-transform: capitalize; +} + +.filter.active { + background: rgba(46, 196, 154, 0.12); + border-color: rgba(46, 196, 154, 0.6); + color: var(--ink); +} + +.task-list { + display: grid; + gap: 10px; + list-style: none; + margin: 0; + padding: 0; +} + +.task-item { + align-items: center; + background: var(--panel); + border: 1px solid var(--line); + border-radius: 8px; + display: grid; + gap: 14px; + grid-template-columns: auto minmax(0, 1fr) auto; + min-height: 70px; + padding: 12px; +} + +.task-item.completed .task-title { + color: var(--muted); + text-decoration: line-through; +} + +.toggle { + background: transparent; + border: 2px solid var(--line); + border-radius: 8px; + height: 30px; + width: 30px; +} + +.task-item.completed .toggle { + background: var(--mint); + border-color: var(--mint); +} + +.task-main { + display: flex; + flex-direction: column; + gap: 6px; + min-width: 0; +} + +.task-title { + font-size: 18px; + font-weight: 760; + overflow-wrap: anywhere; +} + +.priority { + border-radius: 8px; + display: inline-flex; + font-size: 12px; + font-weight: 900; + justify-content: center; + padding: 3px 8px; + text-transform: uppercase; + width: fit-content; +} + +.priority-low { + background: #eaf7f0; + color: #26765a; +} + +.priority-medium { + background: #eef2ff; + color: #485aa6; +} + +.priority-high { + background: #fff1dc; + color: #9b6213; +} + +.task-actions { + display: flex; + flex-wrap: wrap; + gap: 8px; + justify-content: flex-end; +} + +.action-button.danger { + background: #fff0ee; + border-color: #ffd3cc; + color: #a5382c; +} + +.empty-state { + align-items: center; + border: 1px dashed var(--line); + border-radius: 8px; + display: flex; + gap: 12px; + justify-content: space-between; + margin-top: 18px; + padding: 18px; +} + +.empty-state p { + color: var(--muted); + margin: 0; +} + +.architecture-panel { + align-self: start; + min-height: calc(100vh - 56px); + padding: 28px; + position: sticky; + top: 28px; +} + +.architecture-panel p { + color: var(--muted); +} + +.trace-card, +.slot-card { + border-radius: 8px; + margin: 20px 0; + padding: 20px; +} + +.trace-card { + background: #12221c; + color: var(--paper); +} + +.trace-card span, +.slot-card span { + display: block; + font-size: 12px; + font-weight: 900; + margin-bottom: 10px; + text-transform: uppercase; +} + +.trace-card span { + color: #9ddfce; +} + +.trace-card strong { + display: block; + font-size: 28px; + line-height: 1; + margin-bottom: 12px; +} + +.trace-card p { + color: #d6e8e1; + margin-bottom: 0; +} + +.slot-card { + background: #f3f8f5; + border: 1px solid var(--line); +} + +.slot-card code { + color: #315148; + display: block; + font-family: "SFMono-Regular", Consolas, "Liberation Mono", monospace; + font-size: 13px; + overflow-wrap: anywhere; +} + +.flow { + display: grid; + gap: 10px; +} + +.flow div { + background: rgba(46, 196, 154, 0.1); + border: 1px solid rgba(46, 196, 154, 0.28); + border-radius: 8px; + font-weight: 850; + padding: 12px; +} + +@media (max-width: 980px) { + .app-shell { + grid-template-columns: 1fr; + padding: 16px; + } + + .workspace-header, + .empty-state { + align-items: flex-start; + flex-direction: column; + } + + .header-actions { + justify-content: flex-start; + } + + .task-form, + .summary-grid { + grid-template-columns: 1fr; + } + + .architecture-panel { + min-height: 0; + position: static; + } +} + diff --git a/examples/nodejs-react-typescript/frontend/src/types.ts b/examples/nodejs-react-typescript/frontend/src/types.ts new file mode 100644 index 0000000..085e34d --- /dev/null +++ b/examples/nodejs-react-typescript/frontend/src/types.ts @@ -0,0 +1,56 @@ +export type Priority = "low" | "medium" | "high"; + +export interface Task { + id: string; + title: string; + priority: Priority; + completed: boolean; + createdAt: string; + completedAt: string | null; +} + +export interface ApiError { + code: string; + message: string; + details?: Record; +} + +export interface ApiSuccess { + ok: true; + value: Value; +} + +export interface ApiFailure { + ok: false; + error: ApiError; +} + +export type ApiResult = ApiSuccess | ApiFailure; + +export interface AppSlotModel { + tasks: Task[]; + visibleTasks: Task[]; + activeFeature: string; + refreshTasks: (featureName?: string) => Promise; + createExampleTask: () => Promise; +} + +export interface TaskActionsSlotModel { + task: Task; + completeTask: () => Promise; + reopenTask: () => Promise; + deleteTask: () => Promise; +} + +export interface EmptyStateSlotModel { + createExampleTask: () => Promise; +} + +export interface SlotModels { + APP_HEADER_ACTIONS: AppSlotModel; + TASK_SUMMARY_CARDS: AppSlotModel; + TASK_ACTIONS: TaskActionsSlotModel; + EMPTY_STATE_ACTIONS: EmptyStateSlotModel; +} + +export type SlotName = keyof SlotModels; diff --git a/examples/nodejs-react-typescript/frontend/src/vite-env.d.ts b/examples/nodejs-react-typescript/frontend/src/vite-env.d.ts new file mode 100644 index 0000000..11f02fe --- /dev/null +++ b/examples/nodejs-react-typescript/frontend/src/vite-env.d.ts @@ -0,0 +1 @@ +/// diff --git a/examples/nodejs-react-typescript/package-lock.json b/examples/nodejs-react-typescript/package-lock.json new file mode 100644 index 0000000..69102d6 --- /dev/null +++ b/examples/nodejs-react-typescript/package-lock.json @@ -0,0 +1,2254 @@ +{ + "name": "aifa-react-node-slots", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "aifa-react-node-slots", + "version": "0.1.0", + "dependencies": { + "@vitejs/plugin-react": "^4.3.4", + "react": "^19.0.0", + "react-dom": "^19.0.0", + "vite": "^6.0.7" + }, + "devDependencies": { + "@types/node": "^22.10.2", + "@types/react": "^19.0.3", + "@types/react-dom": "^19.0.2", + "tsx": "^4.19.2", + "typescript": "^5.7.2" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz", + "integrity": "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==", + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.29.7", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.7.tgz", + "integrity": "sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.7.tgz", + "integrity": "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==", + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/generator": "^7.29.7", + "@babel/helper-compilation-targets": "^7.29.7", + "@babel/helper-module-transforms": "^7.29.7", + "@babel/helpers": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/template": "^7.29.7", + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7", + "@jridgewell/remapping": "^2.3.5", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/generator": { + "version": "7.29.8", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.8.tgz", + "integrity": "sha512-gZbepsdh3WDtgZKWL+vTPh71LSBrm/Y4/QDZBVCcYfmeTEEuoOYwlSy+G1StfJg+/Zy550u/3TATbm7qDbbMtg==", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.8", + "@babel/types": "^7.29.8", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.29.7.tgz", + "integrity": "sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g==", + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.29.7", + "@babel/helper-validator-option": "^7.29.7", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.29.7.tgz", + "integrity": "sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.29.7.tgz", + "integrity": "sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g==", + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.29.7.tgz", + "integrity": "sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg==", + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7", + "@babel/traverse": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.29.7.tgz", + "integrity": "sha512-G7sHYigPY17oO5SYWnfD/0MTBwVR781S/JI643e/JhUYgVgWE/61SoW3NH9KWUKyKq5LVh3npif99Wkt6j86Jw==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz", + "integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz", + "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.29.7.tgz", + "integrity": "sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.7.tgz", + "integrity": "sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg==", + "license": "MIT", + "dependencies": { + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.8", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.8.tgz", + "integrity": "sha512-E8lTAYNB1KW+FH+VGJuZM1ioAx2E6oVlvQFRrf5P8ZZmsiJXYAD9vTFV7yyEURNzgh1dFqMZuO6tUwcARbqFCA==", + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.8" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-self": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.29.7.tgz", + "integrity": "sha512-TL0hMc9xzy86VD31nUiwzd5otRAcyEPcsegCxolO0PvcXuH1v0kECe/UIznYFihpkvU5wg/jk4v0TTEFfm53fw==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-source": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.29.7.tgz", + "integrity": "sha512-06IyK09H3wi4cGbhDBwp5gUGo0IKtnYa8tyTiephirPCK6fbobVGiXMMI5zLQ4aKEYP3wZ3ArU44o+8KMrSG/Q==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/template": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.29.7.tgz", + "integrity": "sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==", + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.29.8", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.8.tgz", + "integrity": "sha512-I5z7H3bf/41ktsNVLtpN0wAa336HkqIHQ5BuPLEhTkt1jVSyZpeNKIzTgEWmlxjdg81R0IgUCcaE+Ok3NvrfZg==", + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/generator": "^7.29.8", + "@babel/helper-globals": "^7.29.7", + "@babel/parser": "^7.29.8", + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.8", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.8", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.8.tgz", + "integrity": "sha512-Vj1jF3cPfxg7OAfoI7QnVKLoILlm2JF9pnVHrX8qx7AHMiYWT+NDAA7jChlNgRS4WTLc/fD1lXLmPixluj+3Gg==", + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.2.tgz", + "integrity": "sha512-XExcO+dvLKvVtNTibSTBej1NCAbaGhWn9Ww1ZPx80qsahhPFe/8jgWP0IchNe0F3HwkU7n8ejhH8bjonqht8mQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.2.tgz", + "integrity": "sha512-kXXoiPVVGQcnIYGOeaovwOURpniDBpSq4A03qkQ+BMQqtGG6HYap3xne9C1O1yo4TR3qxlCX5IqqmX6fFo2Lqg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.2.tgz", + "integrity": "sha512-5YfKeeI8qWfBZIX+u2xZC3Zlb3Os/gLS2sbEKM+I4ZOcsWmHS2WLysCcQZDAFRslDUU5Oiq44gf6PYN1vGwG5A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.2.tgz", + "integrity": "sha512-O387ite7SzUyCcy3JQX4P4bLtEA7bLLkx+esve5JHnyYfNTxcVpXZo9jhdB0lTKN44gztELTdU7nS8Nr16Fs1Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.2.tgz", + "integrity": "sha512-n4KqkOQrraxHJcgjM1RvwbigfQKIKJVpM7xp+KsxiyUSrRdIXnt73VhrPAx0fV44hgfmIVKjxMN9J1t5jySVkw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.2.tgz", + "integrity": "sha512-uq6suIWYP37qzGddBKPw5QEQPi6HiLGsO7UmkpfyaYNQ3D+rN6w6WfwH+nuqcGXWvawGwxOEroO4YGnFh95azw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.2.tgz", + "integrity": "sha512-n+I0BTSRIoy+d6RPKnEVwql5UwBJolytvY4mAOIEJorKlqgPII8ix6slVVrfZ5Tnj7glIZvloylbB/EJPMWEXw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.2.tgz", + "integrity": "sha512-78XJTJkvPs0kz2w61301PJjXl4g7q3JqiYMZ/M/yVI73EHBrCRTgkhu9oqG7vPqq+a/yadEW8aD+agKlk5xrmg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.2.tgz", + "integrity": "sha512-XlDnu2q5yoqems+xay6wSAcg9DDD7K9RLKZEBOMZm3ckNpJBvOX20tSfby8KfrrhINDyv9V2YVZKY/SpoGJI8w==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.2.tgz", + "integrity": "sha512-pW4AC0P3it8c7do9MVM4p51FzHzdM/TZrerurgRcHJ2WTa1VQ1CIq18xncfpBJw4ojkiZZrKW2yIBWBP92j6Ug==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.2.tgz", + "integrity": "sha512-CYbnj78HsIeA+DhgUKgFCfvNsTHFhMMrinUrMZpDXJXKN8T3XViTZ/+wtHeVxEWY8ewSzTFN+nRmSwO2tZaLUQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.2.tgz", + "integrity": "sha512-buwkd8nsph4R+ajRvw0qM5Hja/TXQow3ptzWO2EbG/cqcIkHloRrdlBtQlshyYGTNFvfkfJ5tpPLVkY4DtsPfQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.2.tgz", + "integrity": "sha512-ZVykbDyk7519VwiNb9Lcj9m8XM6v5V9uKPvrEMkkEedVewf+0itkhahp4HDpgERXhwLRpWFypsGbG/J8s0QjJA==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.2.tgz", + "integrity": "sha512-CAXl+Dtd9UUuJd8pKKdwh6MLm3MUMiqMPmhZ3tTSXPqfyQ3vDl6R5hZdZ/kYojK4ofXtdfSv1tFq8XzWx3heNQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.2.tgz", + "integrity": "sha512-GeXCej4IQtU1B+QlDV8W/RRvbzI3O/Stss+/bCXv4lZls5WGRtu2a+3JkA3i4qIUlMXpcHebWpF8AkJhATowuA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.2.tgz", + "integrity": "sha512-3H1weTYZPxt/WOhByszQZybS9w5lKzUn1FDMsgEChbHWQwHYQQRfBxgCcZvPhjHfKyJjIievvMmEUawJrdY9Dg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.2.tgz", + "integrity": "sha512-4xTZr1FUmSoQW4XIWmit3tzQrUTZM+N3P0XV8xROKYF50XfI7xeO90+1bZvNwxIufQ9hDQVRJH5YhgPVF8A/HQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.2.tgz", + "integrity": "sha512-sSATRjPeDBg3pdgHoQfoYBob11Kk1FGa9lui5RIHZCoCkJa9QKlvl3/vKz2usCmYYjs7ymJR/2Nnsqe+Hjt5nw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.2.tgz", + "integrity": "sha512-lqnzCV+mM0gIADaKihiCg6ifgfU2L3h5E33rNQBN1Y4MaVGnzryzmvvf7UHxprpQdE8hpqLolJ9Rl+SkIRDpyw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.2.tgz", + "integrity": "sha512-AL2qJILH7lNjrDmCQDvdxMfAUIv8KMNZOvrwAQ8i8//ntL9FflhOyMJ8OZSMBb8/AWXe3/5v5S20y3zCoZWKoQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.2.tgz", + "integrity": "sha512-QtiuPytchRyC4rwUKhexJdQKvDuZ6hWloi3igqPQNUJCS1/v9EiO3UTOXR6A3FoMo4fnAKbWJdqaIwhOzh8qEw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.2.tgz", + "integrity": "sha512-WkhYDmpTjLvGlScA1rwjRUmhl4k8oXR3cIbtqWmELgU/dFeHHlEllxDvdWcNJV9rbzCexB5vz8gtNewWLgCT7Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.2.tgz", + "integrity": "sha512-GPMSkTOtMnv2U2F8gxe4Io6qmVs+YKyp832Etqqxr0hFngmXQ3rzwytelm3GIn7T4VviRUlf3sOgBOiTdvaf7g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.2.tgz", + "integrity": "sha512-PIhhEkE9uPBleRBrQEJpUn7MBnibZzbGzYWPmY3x+YoVg/95zbjB4CxPPOQ8l5tYYM4mMaCthF8/1DIfBQQyWQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.2.tgz", + "integrity": "sha512-YmJbfTlvU7Sdn9BB+4PRES4oB6pxgS37MAONj+hBr/cpXS1aBPKXxNnDbu+QCWPj0o9dgyxeq79g6c5P8KeuYA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.2.tgz", + "integrity": "sha512-5ebpxr3nWMzrL/rnUI755Jkuee0bHL/Gq0WTF9lvcpv73wAp5eu8MfBUgWK9bhWvZjj7yX8etf/8tI8Ney695g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@napi-rs/lzma-linux-x64-gnu": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/@napi-rs/lzma-linux-x64-gnu/-/lzma-linux-x64-gnu-1.5.1.tgz", + "integrity": "sha512-oTXEIha4SsuXdTA4Iyskj0kpdx2yVXdhd75c2v3xGrHFfVMsbhTPZU/nMPL4sWKo4pBHm3aucLaqGlF696dTyQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^22.20 || ^24.12 || >=25" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.0-beta.27", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz", + "integrity": "sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==", + "license": "MIT" + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.62.4.tgz", + "integrity": "sha512-RrPokAb7dmbxFoeO3TloqHyOjgye8RkBhSqmp4aJMIex4c9r46ZstPnleDQOq1t46VOVjwIuwNogIqbodV1Vvg==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.62.4.tgz", + "integrity": "sha512-JKuJc+pnpks2pjy7L/N3v/cAkZxYlnmuZoD840ldbMI5KDbC4iO9NKwPKYdjYFCMAIIlBzYSFHxIJVYzRo2/8A==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.62.4.tgz", + "integrity": "sha512-krw5uS2STmvJ02x0uTXHbqQNuz+9eZ1iw+qXk9dmW2gvV4jV7O2hEoOnuhFrpOPiel1mBFtqbxYZZtC46hXLOw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.62.4.tgz", + "integrity": "sha512-wsTxtgApb4PrOsNJIm0FZ1h3WvCC+k9uxLJ4ad75hgoS4NiRes2SoJFlDAyMwiUY8IssDqGcHbXuN0sx1tfF1A==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.62.4.tgz", + "integrity": "sha512-GUOnQlyZe3yAXhWOtOMsn5Qkrv5E5mZXa0thbARWi5Ei2szlVXJFQhddZ4HbAzh8q92w5twp+CQvs/eFanz9YQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.62.4.tgz", + "integrity": "sha512-/Y7f3QuxjzPKsjA/rfEDa3+0vXqyjmJ50Ln8dPpCmWkKTrUoWHG1cWhTqaAMLob2m2nESWuC7yGrREz019Ztqg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.62.4.tgz", + "integrity": "sha512-81wiiX3v7aqy+T+bT61TJ78yJjRquqFFTTbAPt08imfQQzkPIW8t6aJbkTagtCCrXMNc9D66+geqlK7ydLPNqA==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.62.4.tgz", + "integrity": "sha512-9kmDIvNZqdoHOBZgNtpTBeLWYO/LVipM3H/j62P8848/l/VPEQL6N3uxU9pvP1oZAsXyC2MEnFP3ovRjo7WYNQ==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.62.4.tgz", + "integrity": "sha512-CcnXHWnXg69g+DX5VWL3FHts3qMRN2uVEHX+BZvGLdd07/gXkn3ePjYtO1LDJvxkGKVHMclKBRa1QUTH+6toYQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.62.4.tgz", + "integrity": "sha512-iFOibiHnTRuhrWLlRsOQFdZJJIa7S8OwkneJr4ocALP16u5yk6lWLINFwhHaEqBFMsKDUZofLkGos7+CPzGB3g==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.62.4.tgz", + "integrity": "sha512-XnWYMI7euHlb5a871xPja+Gm7DRCFU+FGRrtS2sMq9N8FvqtpagUy6gD4YOemC5MRk9xbh8+jYMEJbigFQwsgA==", + "cpu": [ + "loong64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.62.4.tgz", + "integrity": "sha512-qGDAlO0U8xedCcsdRm9oaoQY8DAx/QT7uIxJWhCdx0ceIWX783UC9QSYkdpzAe29wNiVfp24+bZdQmn49o45SQ==", + "cpu": [ + "loong64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.62.4.tgz", + "integrity": "sha512-ru4H6ezD7ysA5EiEK6qkkaEb4modH8CTej6kUy/gQi20u3kB3G7Zn8snXXkeJSCOFKG/rbPPtM/+9Wgas1961w==", + "cpu": [ + "ppc64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.62.4.tgz", + "integrity": "sha512-2W4MO5WQVJnbJaZdvDb9rhBDuFU1nKIepPFpJUBsTh2k1YY2g+ODViaWuyOAjQ5cOP7NvrvLzt3wvHOoiAvc7w==", + "cpu": [ + "ppc64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.62.4.tgz", + "integrity": "sha512-+fxjfuoAmVMCYV5QyjoIpu0cp5DOiOTeqYFk1AVaxGr+/ravWLX89XfQmptsoWcaVy/TGf2hexzbUOrCQIL1CQ==", + "cpu": [ + "riscv64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.62.4.tgz", + "integrity": "sha512-jTn8JfHGL4djjFxPuM06LmNUJDsst2jeVlsd9OmIH6zc5sC9K6rIuO4YajXatLUpBmBKl6b35ro1QZocLi+tcA==", + "cpu": [ + "riscv64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.62.4.tgz", + "integrity": "sha512-oCJCJL4pXsoDcP2QZ+JVlPTIRc6266zsIaeJJsWImmF7HO0W8nb6HuSgZlMWxJwaPf8ehbSw8yo0EUw925hKsA==", + "cpu": [ + "s390x" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.62.4.tgz", + "integrity": "sha512-W69hukhZ3KKNRCaMIEzKvcFye42hh0FE1+YoYaf5+Ikacuftoco6yO/xouz0hc5d5W/s3yBro5jRiuEE/Q5vUw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.62.4.tgz", + "integrity": "sha512-qiXbGG2jkjXhzXpsFZSR2Xpb8DN/UaxYsbb/STbuR/6fpaDgRmmaq1B/LmtF2wQFOFOSsK2jdE0RZ3a0zHn4QA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.62.4.tgz", + "integrity": "sha512-nWeM//hxv8mIo6jD7Hu4o48DVmV9pbV6gsKaWU+4NFyqHoPKwrkRiZGLKUhOBk8qNmDmpwFtPKg80Bo/Tn4xiQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.62.4.tgz", + "integrity": "sha512-s62SQ/vgsRSvMwDkOEfTqfgASF0f26ZNaQuTA6Aok5lrikf89yI2W0gFHvZb2Jpgc6N8JnOKZgCK2iciO3CsxQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.62.4.tgz", + "integrity": "sha512-J6wGf8TVGbXJq+HH+ttTvrcfNKPbuZecV6KT1B8I18BC5IURUh5kl4Yl5OEP5eFIUoI5BWxCsyYMhFsDx8kekw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.62.4.tgz", + "integrity": "sha512-zmfrQd/0wu6oJs8Vq8KwY/YtsKSsLtKe/HwAP4Wqy8LhWjeT55fHRAkOhYQ12wI3ayS4Tt12d5CDRD7N96SAYQ==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.62.4.tgz", + "integrity": "sha512-qPzHqdj9rfUD+w79dtE07zi/kFwKyCJqplp5K5ygeLTp7jLpAoc16OAH39HSmRC9UpozaecsleI8uAdEj6v2yw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.62.4.tgz", + "integrity": "sha512-zD6NdeWEByGE9QF9vCrlJ5YQB4oq9q91kPZS37Jwj5hOkvR1lTBSpsKhKDw4IJtbQ35LsTS1HD9DZYGKIshU1Q==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", + "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", + "license": "MIT", + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", + "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==", + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.2" + } + }, + "node_modules/@types/estree": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "22.20.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.20.1.tgz", + "integrity": "sha512-EANqOCF9QFyra+4pfxUcX9STKJpCLjMbObVzljIJomAWSnuSIEAvyzEU53GaajbXJEgdh0iEcPL+DGvpUd4k1Q==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/@types/react": { + "version": "19.2.18", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.18.tgz", + "integrity": "sha512-AnzbBERsrLKtk2XSfTbYRLjQPdy116Sty4q+T+Bp3IC4l6jNBvreVPAHmpq9qhXQM7CXZPjLVmGMw9sy+hxQ3w==", + "dev": true, + "license": "MIT", + "dependencies": { + "csstype": "^3.2.2" + } + }, + "node_modules/@types/react-dom": { + "version": "19.2.4", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.4.tgz", + "integrity": "sha512-Bsc+QHgp+P/F02XDzNCY9jnZNCUuLki36KT7VKrTXXLdHf+vHMNZnW1rVu5DNW/rCK+fya3DATySbLM4yhtKUw==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "^19.2.0" + } + }, + "node_modules/@vitejs/plugin-react": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.7.0.tgz", + "integrity": "sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA==", + "license": "MIT", + "dependencies": { + "@babel/core": "^7.28.0", + "@babel/plugin-transform-react-jsx-self": "^7.27.1", + "@babel/plugin-transform-react-jsx-source": "^7.27.1", + "@rolldown/pluginutils": "1.0.0-beta.27", + "@types/babel__core": "^7.20.5", + "react-refresh": "^0.17.0" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "peerDependencies": { + "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" + } + }, + "node_modules/baseline-browser-mapping": { + "version": "2.11.14", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.11.14.tgz", + "integrity": "sha512-JyJ954WzuIR8/FFzX0o5krdSTrBAkcCSRfWSleRsIHSWV+cZe2FI1PKggVkFke1hBldRs+LRxUczzE9iPmgZww==", + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/browserslist": { + "version": "4.28.8", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.8.tgz", + "integrity": "sha512-V2NpofLblG64mfOtSgDhOJESZEGogzDMBv/q+W6oc4LXWP/q75eOXoOaaOu1EOadB9U4Bwx/e0yzbvwKH8zalA==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "baseline-browser-mapping": "^2.11.12", + "caniuse-lite": "^1.0.30001809", + "electron-to-chromium": "^1.5.402", + "node-releases": "^2.0.53", + "update-browserslist-db": "^1.3.0" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001809", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001809.tgz", + "integrity": "sha512-xxWVywk6a6Arlk+hymeycyn/VgqEfLDxupvhH/xiY5SJ/18kmi9o6MiO320DCUzypORHLtvh0I4i04tUhCNHNQ==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "license": "MIT" + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/electron-to-chromium": { + "version": "1.5.406", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.406.tgz", + "integrity": "sha512-hWH5ORBi3d0IipnMh7BN5GDTaAmrSSSWmznwt2zltdiRNEWoEQyTwF0FFSBxzHO7hLSRT6loQu3IQGV0wg/Tvg==", + "license": "ISC" + }, + "node_modules/esbuild": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.2.tgz", + "integrity": "sha512-HKVLS8dvII+xoKW9kmqxbRKrnWEXfJJr/FZhhJmiqIB0e053QNYFqOBouTMO/k5sID4MvCiUCvv8b9M4h32wIA==", + "devOptional": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.28.2", + "@esbuild/android-arm": "0.28.2", + "@esbuild/android-arm64": "0.28.2", + "@esbuild/android-x64": "0.28.2", + "@esbuild/darwin-arm64": "0.28.2", + "@esbuild/darwin-x64": "0.28.2", + "@esbuild/freebsd-arm64": "0.28.2", + "@esbuild/freebsd-x64": "0.28.2", + "@esbuild/linux-arm": "0.28.2", + "@esbuild/linux-arm64": "0.28.2", + "@esbuild/linux-ia32": "0.28.2", + "@esbuild/linux-loong64": "0.28.2", + "@esbuild/linux-mips64el": "0.28.2", + "@esbuild/linux-ppc64": "0.28.2", + "@esbuild/linux-riscv64": "0.28.2", + "@esbuild/linux-s390x": "0.28.2", + "@esbuild/linux-x64": "0.28.2", + "@esbuild/netbsd-arm64": "0.28.2", + "@esbuild/netbsd-x64": "0.28.2", + "@esbuild/openbsd-arm64": "0.28.2", + "@esbuild/openbsd-x64": "0.28.2", + "@esbuild/openharmony-arm64": "0.28.2", + "@esbuild/sunos-x64": "0.28.2", + "@esbuild/win32-arm64": "0.28.2", + "@esbuild/win32-ia32": "0.28.2", + "@esbuild/win32-x64": "0.28.2" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "license": "MIT" + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.18", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.18.tgz", + "integrity": "sha512-DTg4MJbGMWkfi6VZFdNt2/caMbQy4Ou+Op/hJQvGEWcnVfoA1QA+xzRKAzw9jD6+GVOOeYr/mIcuDSdug6F6+w==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/node-releases": { + "version": "2.0.53", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.53.tgz", + "integrity": "sha512-D9UOmYG3UH1V+ENW56t5QXBwJw1YEY18ruVeus89Rw+SyIgjPkCO84bRzO3uNIYosJbNwiabWVn48o3uJLjxFQ==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.5.tgz", + "integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/postcss": { + "version": "8.5.26", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.26.tgz", + "integrity": "sha512-u82N74LFzG8ca+dD8puPnplTXoGH4fTPpVGuIbt36G3qvNlkvfD0lEAZSxaly3KX8TS/L1A1gsCEmvKmBcVbkQ==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.17", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/react": { + "version": "19.2.8", + "resolved": "https://registry.npmjs.org/react/-/react-19.2.8.tgz", + "integrity": "sha512-PWaYA1L/q9u2u7xYQi+Y3L3Yfnie7XyLeaJICV1MGD6LprsBxcAqGjYyr0eY3p+QdsA+x/Irkt4Qif8D63+Sbw==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "19.2.8", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.8.tgz", + "integrity": "sha512-rVprimfGBG3DR+Tq0IQG2DT5PxKth1WIGDmj5yPmlzr4YBe7uyE+Du4oVqTDXZSHGGGXRtTJEGSSePyQCMBglQ==", + "license": "MIT", + "dependencies": { + "scheduler": "^0.27.0" + }, + "peerDependencies": { + "react": "^19.2.8" + } + }, + "node_modules/react-refresh": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz", + "integrity": "sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/rollup": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.62.4.tgz", + "integrity": "sha512-RXOqwaPsBGjMNMa4sQjDjHieHEZDFoj/Rdr46l2MU5DfEs16wHJPC2RPTPHWhNl+M3aI472LLqFkFKut4SblOg==", + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.9" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@napi-rs/lzma-linux-x64-gnu": "1.5.1", + "@rollup/rollup-android-arm-eabi": "4.62.4", + "@rollup/rollup-android-arm64": "4.62.4", + "@rollup/rollup-darwin-arm64": "4.62.4", + "@rollup/rollup-darwin-x64": "4.62.4", + "@rollup/rollup-freebsd-arm64": "4.62.4", + "@rollup/rollup-freebsd-x64": "4.62.4", + "@rollup/rollup-linux-arm-gnueabihf": "4.62.4", + "@rollup/rollup-linux-arm-musleabihf": "4.62.4", + "@rollup/rollup-linux-arm64-gnu": "4.62.4", + "@rollup/rollup-linux-arm64-musl": "4.62.4", + "@rollup/rollup-linux-loong64-gnu": "4.62.4", + "@rollup/rollup-linux-loong64-musl": "4.62.4", + "@rollup/rollup-linux-ppc64-gnu": "4.62.4", + "@rollup/rollup-linux-ppc64-musl": "4.62.4", + "@rollup/rollup-linux-riscv64-gnu": "4.62.4", + "@rollup/rollup-linux-riscv64-musl": "4.62.4", + "@rollup/rollup-linux-s390x-gnu": "4.62.4", + "@rollup/rollup-linux-x64-gnu": "4.62.4", + "@rollup/rollup-linux-x64-musl": "4.62.4", + "@rollup/rollup-openbsd-x64": "4.62.4", + "@rollup/rollup-openharmony-arm64": "4.62.4", + "@rollup/rollup-win32-arm64-msvc": "4.62.4", + "@rollup/rollup-win32-ia32-msvc": "4.62.4", + "@rollup/rollup-win32-x64-gnu": "4.62.4", + "@rollup/rollup-win32-x64-msvc": "4.62.4", + "fsevents": "~2.3.2" + } + }, + "node_modules/scheduler": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", + "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==", + "license": "MIT" + }, + "node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.17", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", + "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tsx": { + "version": "4.23.12", + "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.23.12.tgz", + "integrity": "sha512-FDf4L4sYzKtzWYhU/Xm0AQFdTjdIxNo9ElTf2mxXM6k8YMHXzYUe4yODVaXP4V9uMFbVg8c0qyBccK2OOxb45Q==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "esbuild": "~0.28.0" + }, + "bin": { + "tsx": "dist/cli.mjs" + }, + "engines": { + "node": ">=18.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/update-browserslist-db": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.3.1.tgz", + "integrity": "sha512-ZZ61DsRsOnakl74HAmp3oSN4aXUmEWXf+i/yv0h7tIBfICc3VdrFErQKUUKPgu3AMsTUMbcongALEN4l6GSUrQ==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/vite": { + "version": "6.4.3", + "resolved": "https://registry.npmjs.org/vite/-/vite-6.4.3.tgz", + "integrity": "sha512-NTKlcQjlAK7MlQoyb6LgaqHc8sso/pVyUJYWMws3jg21uTJw/LddqIFPcPqP6PzpgbIcZyKI85sFE4HBrQDA8A==", + "license": "MIT", + "dependencies": { + "esbuild": "^0.25.0", + "fdir": "^6.4.4", + "picomatch": "^4.0.2", + "postcss": "^8.5.3", + "rollup": "^4.34.9", + "tinyglobby": "^0.2.13" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", + "jiti": ">=1.21.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/vite/node_modules/@esbuild/aix-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz", + "integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==", + "cpu": [ + "ppc64" + ], + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/android-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.12.tgz", + "integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/android-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz", + "integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/android-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.12.tgz", + "integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/darwin-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz", + "integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/darwin-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz", + "integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/freebsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz", + "integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/freebsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz", + "integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz", + "integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz", + "integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz", + "integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-loong64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz", + "integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==", + "cpu": [ + "loong64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-mips64el": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz", + "integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==", + "cpu": [ + "mips64el" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz", + "integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==", + "cpu": [ + "ppc64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-riscv64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz", + "integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==", + "cpu": [ + "riscv64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-s390x": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz", + "integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==", + "cpu": [ + "s390x" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz", + "integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/netbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz", + "integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/netbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz", + "integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/openbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz", + "integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/openbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz", + "integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/openharmony-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz", + "integrity": "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/sunos-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz", + "integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/win32-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz", + "integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/win32-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz", + "integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/win32-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz", + "integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/esbuild": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz", + "integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==", + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.25.12", + "@esbuild/android-arm": "0.25.12", + "@esbuild/android-arm64": "0.25.12", + "@esbuild/android-x64": "0.25.12", + "@esbuild/darwin-arm64": "0.25.12", + "@esbuild/darwin-x64": "0.25.12", + "@esbuild/freebsd-arm64": "0.25.12", + "@esbuild/freebsd-x64": "0.25.12", + "@esbuild/linux-arm": "0.25.12", + "@esbuild/linux-arm64": "0.25.12", + "@esbuild/linux-ia32": "0.25.12", + "@esbuild/linux-loong64": "0.25.12", + "@esbuild/linux-mips64el": "0.25.12", + "@esbuild/linux-ppc64": "0.25.12", + "@esbuild/linux-riscv64": "0.25.12", + "@esbuild/linux-s390x": "0.25.12", + "@esbuild/linux-x64": "0.25.12", + "@esbuild/netbsd-arm64": "0.25.12", + "@esbuild/netbsd-x64": "0.25.12", + "@esbuild/openbsd-arm64": "0.25.12", + "@esbuild/openbsd-x64": "0.25.12", + "@esbuild/openharmony-arm64": "0.25.12", + "@esbuild/sunos-x64": "0.25.12", + "@esbuild/win32-arm64": "0.25.12", + "@esbuild/win32-ia32": "0.25.12", + "@esbuild/win32-x64": "0.25.12" + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "license": "ISC" + } + } +} diff --git a/examples/nodejs-react-typescript/package.json b/examples/nodejs-react-typescript/package.json new file mode 100644 index 0000000..5d32816 --- /dev/null +++ b/examples/nodejs-react-typescript/package.json @@ -0,0 +1,30 @@ +{ + "name": "aifa-react-node-slots", + "version": "0.1.0", + "description": "A React and Node.js example app built with AI Atomic Feature Architecture and slot-based UI contributions.", + "type": "module", + "private": true, + "scripts": { + "api": "tsx backend/server.ts", + "dev": "vite frontend --host 127.0.0.1", + "build": "tsc --noEmit && vite build frontend --outDir ../dist", + "start": "tsx backend/server.ts", + "test": "tsx --test" + }, + "dependencies": { + "@vitejs/plugin-react": "^4.3.4", + "vite": "^6.0.7", + "react": "^19.0.0", + "react-dom": "^19.0.0" + }, + "devDependencies": { + "@types/node": "^22.10.2", + "@types/react": "^19.0.3", + "@types/react-dom": "^19.0.2", + "tsx": "^4.19.2", + "typescript": "^5.7.2" + }, + "engines": { + "node": ">=20" + } +} diff --git a/examples/nodejs-react-typescript/test/slotRegistry.test.js b/examples/nodejs-react-typescript/test/slotRegistry.test.js new file mode 100644 index 0000000..f955cb3 --- /dev/null +++ b/examples/nodejs-react-typescript/test/slotRegistry.test.js @@ -0,0 +1,45 @@ +import assert from "node:assert/strict"; +import test from "node:test"; + +import { getSlotContributions, registerSlotContribution } from "../frontend/src/slots/slotRegistry.ts"; + +test("sorts slot contributions by order", () => { + registerSlotContribution({ + slot: "TEST_SLOT", + name: "second", + order: 20, + render: () => null, + }); + registerSlotContribution({ + slot: "TEST_SLOT", + name: "first", + order: 10, + render: () => null, + }); + + assert.deepEqual( + getSlotContributions("TEST_SLOT").map((contribution) => contribution.name), + ["first", "second"], + ); +}); + +test("replaces duplicate slot contribution names", () => { + registerSlotContribution({ + slot: "REPLACE_SLOT", + name: "action", + order: 20, + render: () => "old", + }); + registerSlotContribution({ + slot: "REPLACE_SLOT", + name: "action", + order: 10, + render: () => "new", + }); + + const contributions = getSlotContributions("REPLACE_SLOT"); + + assert.equal(contributions.length, 1); + assert.equal(contributions[0].order, 10); + assert.equal(contributions[0].render(), "new"); +}); diff --git a/examples/nodejs-react-typescript/test/taskFeatures.test.js b/examples/nodejs-react-typescript/test/taskFeatures.test.js new file mode 100644 index 0000000..8062c64 --- /dev/null +++ b/examples/nodejs-react-typescript/test/taskFeatures.test.js @@ -0,0 +1,115 @@ +import assert from "node:assert/strict"; +import test from "node:test"; + +import { completeTask } from "../backend/features/completeTask.ts"; +import { createTask } from "../backend/features/createTask.ts"; +import { deleteTask } from "../backend/features/deleteTask.ts"; +import { listTasks } from "../backend/features/listTasks.ts"; +import { reopenTask } from "../backend/features/reopenTask.ts"; +import { createAifaRuntime } from "../backend/runtime/aifaRuntime.ts"; +import { createMemoryTaskStore } from "../backend/storage/memoryTaskStore.ts"; + +const actor = { + id: "test-user", +}; + +function createRuntime(seed = []) { + let nextId = 1; + + return createAifaRuntime({ + taskStore: createMemoryTaskStore(seed), + idFactory: { + next() { + const id = `task-${nextId}`; + nextId += 1; + return id; + }, + }, + clock: () => new Date("2026-08-14T10:00:00.000Z"), + }); +} + +test("creates an active task with priority", async () => { + const runtime = createRuntime(); + + const result = await runtime.run(createTask, { title: " Build slot UI ", priority: "high" }, actor); + + assert.equal(result.ok, true); + assert.equal(result.value.task.id, "task-1"); + assert.equal(result.value.task.title, "Build slot UI"); + assert.equal(result.value.task.priority, "high"); + assert.equal(result.value.task.completed, false); + assert.equal(runtime.auditLog[0].type, "task.created"); +}); + +test("defaults unknown priority to medium", async () => { + const runtime = createRuntime(); + + const result = await runtime.run(createTask, { title: "Review contract", priority: "urgent" }, actor); + + assert.equal(result.value.task.priority, "medium"); +}); + +test("rejects empty task titles", async () => { + const runtime = createRuntime(); + + const result = await runtime.run(createTask, { title: " " }, actor); + + assert.equal(result.ok, false); + assert.equal(result.error.code, "invalid_title"); +}); + +test("lists tasks in creation order", async () => { + const runtime = createRuntime([ + createSeedTask("task-2", "Second", "medium", "2026-08-14T10:02:00.000Z"), + createSeedTask("task-1", "First", "low", "2026-08-14T10:01:00.000Z"), + ]); + + const result = await runtime.run(listTasks, {}, actor); + + assert.deepEqual( + result.value.tasks.map((task) => task.id), + ["task-1", "task-2"], + ); +}); + +test("completes, reopens, and deletes a task", async () => { + const runtime = createRuntime([createSeedTask("task-1", "Ship example")]); + + const completeResult = await runtime.run(completeTask, { taskId: "task-1" }, actor); + const reopenResult = await runtime.run(reopenTask, { taskId: "task-1" }, actor); + const deleteResult = await runtime.run(deleteTask, { taskId: "task-1" }, actor); + const listResult = await runtime.run(listTasks, {}, actor); + + assert.equal(completeResult.value.task.completed, true); + assert.equal(reopenResult.value.task.completed, false); + assert.equal(deleteResult.value.deleted, true); + assert.equal(listResult.value.tasks.length, 0); + assert.deepEqual( + runtime.auditLog.map((entry) => entry.type), + ["task.completed", "task.reopened", "task.deleted"], + ); +}); + +test("returns not_found for missing task actions", async () => { + const runtime = createRuntime(); + + const completeResult = await runtime.run(completeTask, { taskId: "missing" }, actor); + const reopenResult = await runtime.run(reopenTask, { taskId: "missing" }, actor); + const deleteResult = await runtime.run(deleteTask, { taskId: "missing" }, actor); + + assert.equal(completeResult.error.code, "not_found"); + assert.equal(reopenResult.error.code, "not_found"); + assert.equal(deleteResult.error.code, "not_found"); +}); + +function createSeedTask(id, title, priority = "medium", createdAt = "2026-08-14T10:00:00.000Z") { + return { + id, + title, + priority, + completed: false, + createdAt, + completedAt: null, + }; +} diff --git a/examples/nodejs-react-typescript/tsconfig.json b/examples/nodejs-react-typescript/tsconfig.json new file mode 100644 index 0000000..321cb10 --- /dev/null +++ b/examples/nodejs-react-typescript/tsconfig.json @@ -0,0 +1,20 @@ +{ + "compilerOptions": { + "target": "ES2022", + "useDefineForClassFields": true, + "lib": ["ES2022", "DOM", "DOM.Iterable"], + "allowJs": false, + "skipLibCheck": true, + "esModuleInterop": true, + "allowSyntheticDefaultImports": true, + "strict": true, + "forceConsistentCasingInFileNames": true, + "module": "ESNext", + "moduleResolution": "Bundler", + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "jsx": "react-jsx" + }, + "include": ["frontend/src", "backend"] +} diff --git a/examples/vanilla-js/README.md b/examples/vanilla-js/README.md new file mode 100644 index 0000000..5e70584 --- /dev/null +++ b/examples/vanilla-js/README.md @@ -0,0 +1,79 @@ +# AIFA Full-Stack Task Board + +This is a fully functional example app built with **AI Atomic Feature Architecture**. + +It has: + +- a Node.js backend +- a browser frontend +- file-backed persistence +- atomic backend features +- runtime-provided capabilities +- ticket contracts +- behavioral tests + +## Run + +```sh +npm test +npm start +``` + +Open: + +```txt +http://localhost:3000 +``` + +## Architecture + +Each app action maps to one AIFA feature: + +| UI Action | API Route | Atomic Feature | +| --- | --- | --- | +| Create task | `POST /api/tasks` | `createTask` | +| List tasks | `GET /api/tasks` | `listTasks` | +| Complete task | `POST /api/tasks/:id/complete` | `completeTask` | +| Reopen task | `POST /api/tasks/:id/reopen` | `reopenTask` | +| Delete task | `DELETE /api/tasks/:id` | `deleteTask` | + +The feature does not know HTTP, files, local storage, routing, or the frontend. + +The feature only knows: + +- its input +- its output +- its allowed capabilities +- its business rules + +## Folder Structure + +```txt +backend/ + features/ Atomic features + runtime/ AIFA runtime and HTTP adapter + storage/ File-backed task store + server.js Backend entrypoint +frontend/ + index.html Browser UI + styles.css UI styling + app.js Frontend behavior +examples/tickets/ + *.md Human/AI-readable ticket contracts +test/ + *.test.js Behavioral feature tests +``` + +## Why This Example Matters + +Without AIFA, "add task completion" could require exploring routes, controllers, services, storage, frontend state, and tests. + +With AIFA, the implementation target is small: + +1. read the ticket +2. implement one feature +3. use only declared capabilities +4. run behavioral tests + +The runtime owns infrastructure. The feature owns behavior. + diff --git a/examples/vanilla-js/backend/features/completeTask.js b/examples/vanilla-js/backend/features/completeTask.js new file mode 100644 index 0000000..5af0b76 --- /dev/null +++ b/examples/vanilla-js/backend/features/completeTask.js @@ -0,0 +1,45 @@ +import { defineFeature } from "../runtime/feature.js"; + +export const completeTask = defineFeature({ + name: "complete-task", + description: "Mark one existing task as completed.", + input: { + taskId: "string", + }, + output: { + task: "Task", + }, + capabilities: ["clock.now", "task.load", "task.save", "audit.record"], + + async execute(context) { + const task = await context.capabilities["task.load"]({ + taskId: context.input.taskId, + }); + + if (!task) { + return context.fail("not_found", "Task was not found", { + taskId: context.input.taskId, + }); + } + + if (task.completed) { + return context.ok({ task }); + } + + const completedTask = { + ...task, + completed: true, + completedAt: await context.capabilities["clock.now"](), + }; + + await context.capabilities["task.save"]({ task: completedTask }); + await context.capabilities["audit.record"]({ + type: "task.completed", + actorId: context.actor.id, + taskId: task.id, + }); + + return context.ok({ task: completedTask }); + }, +}); + diff --git a/examples/vanilla-js/backend/features/createTask.js b/examples/vanilla-js/backend/features/createTask.js new file mode 100644 index 0000000..154b70a --- /dev/null +++ b/examples/vanilla-js/backend/features/createTask.js @@ -0,0 +1,40 @@ +import { defineFeature } from "../runtime/feature.js"; + +export const createTask = defineFeature({ + name: "create-task", + description: "Create one task with a non-empty title.", + input: { + title: "string", + }, + output: { + task: "Task", + }, + capabilities: ["id.create", "clock.now", "task.create", "audit.record"], + + async execute(context) { + const title = context.input.title?.trim() ?? ""; + + if (!title) { + return context.fail("invalid_title", "Task title is required"); + } + + const now = await context.capabilities["clock.now"](); + const task = { + id: await context.capabilities["id.create"](), + title, + completed: false, + createdAt: now, + completedAt: null, + }; + + await context.capabilities["task.create"]({ task }); + await context.capabilities["audit.record"]({ + type: "task.created", + actorId: context.actor.id, + taskId: task.id, + }); + + return context.ok({ task }); + }, +}); + diff --git a/examples/vanilla-js/backend/features/deleteTask.js b/examples/vanilla-js/backend/features/deleteTask.js new file mode 100644 index 0000000..07c4936 --- /dev/null +++ b/examples/vanilla-js/backend/features/deleteTask.js @@ -0,0 +1,37 @@ +import { defineFeature } from "../runtime/feature.js"; + +export const deleteTask = defineFeature({ + name: "delete-task", + description: "Delete one existing task.", + input: { + taskId: "string", + }, + output: { + deleted: "boolean", + }, + capabilities: ["task.load", "task.delete", "audit.record"], + + async execute(context) { + const task = await context.capabilities["task.load"]({ + taskId: context.input.taskId, + }); + + if (!task) { + return context.fail("not_found", "Task was not found", { + taskId: context.input.taskId, + }); + } + + await context.capabilities["task.delete"]({ + taskId: task.id, + }); + await context.capabilities["audit.record"]({ + type: "task.deleted", + actorId: context.actor.id, + taskId: task.id, + }); + + return context.ok({ deleted: true }); + }, +}); + diff --git a/examples/vanilla-js/backend/features/listTasks.js b/examples/vanilla-js/backend/features/listTasks.js new file mode 100644 index 0000000..39b97d4 --- /dev/null +++ b/examples/vanilla-js/backend/features/listTasks.js @@ -0,0 +1,18 @@ +import { defineFeature } from "../runtime/feature.js"; + +export const listTasks = defineFeature({ + name: "list-tasks", + description: "List tasks in creation order.", + input: {}, + output: { + tasks: "Task[]", + }, + capabilities: ["task.list"], + + async execute(context) { + const tasks = await context.capabilities["task.list"](); + + return context.ok({ tasks }); + }, +}); + diff --git a/examples/vanilla-js/backend/features/reopenTask.js b/examples/vanilla-js/backend/features/reopenTask.js new file mode 100644 index 0000000..b6a3875 --- /dev/null +++ b/examples/vanilla-js/backend/features/reopenTask.js @@ -0,0 +1,45 @@ +import { defineFeature } from "../runtime/feature.js"; + +export const reopenTask = defineFeature({ + name: "reopen-task", + description: "Mark one completed task as active again.", + input: { + taskId: "string", + }, + output: { + task: "Task", + }, + capabilities: ["task.load", "task.save", "audit.record"], + + async execute(context) { + const task = await context.capabilities["task.load"]({ + taskId: context.input.taskId, + }); + + if (!task) { + return context.fail("not_found", "Task was not found", { + taskId: context.input.taskId, + }); + } + + if (!task.completed) { + return context.ok({ task }); + } + + const reopenedTask = { + ...task, + completed: false, + completedAt: null, + }; + + await context.capabilities["task.save"]({ task: reopenedTask }); + await context.capabilities["audit.record"]({ + type: "task.reopened", + actorId: context.actor.id, + taskId: task.id, + }); + + return context.ok({ task: reopenedTask }); + }, +}); + diff --git a/examples/vanilla-js/backend/runtime/aifaRuntime.js b/examples/vanilla-js/backend/runtime/aifaRuntime.js new file mode 100644 index 0000000..b6c8d8a --- /dev/null +++ b/examples/vanilla-js/backend/runtime/aifaRuntime.js @@ -0,0 +1,78 @@ +import { fail, ok } from "./feature.js"; + +export function createAifaRuntime({ taskStore, idFactory = createIdFactory(), clock = () => new Date() }) { + const auditLog = []; + + const allCapabilities = { + "id.create": async () => idFactory.next(), + "clock.now": async () => clock().toISOString(), + "task.create": async ({ task }) => taskStore.create(task), + "task.load": async ({ taskId }) => taskStore.load(taskId), + "task.save": async ({ task }) => taskStore.save(task), + "task.delete": async ({ taskId }) => taskStore.delete(taskId), + "task.list": async () => taskStore.list(), + "audit.record": async (entry) => { + auditLog.push({ + ...entry, + recordedAt: clock().toISOString(), + }); + }, + }; + + function bindCapabilities(feature) { + const allowed = new Set(feature.capabilities); + + return new Proxy( + {}, + { + get(_target, capabilityName) { + if (typeof capabilityName !== "string") { + return undefined; + } + + if (!allowed.has(capabilityName)) { + return async () => + fail("capability_not_allowed", `Feature '${feature.name}' cannot use '${capabilityName}'`); + } + + const capability = allCapabilities[capabilityName]; + + if (!capability) { + return async () => fail("capability_missing", `Runtime does not provide '${capabilityName}'`); + } + + return capability; + }, + }, + ); + } + + async function run(feature, input, actor = { id: "demo-user" }, metadata = {}) { + return feature.execute({ + input, + actor, + metadata, + capabilities: bindCapabilities(feature), + ok, + fail, + }); + } + + return { + auditLog, + run, + }; +} + +function createIdFactory() { + return { + next() { + if (globalThis.crypto?.randomUUID) { + return globalThis.crypto.randomUUID(); + } + + return `task-${Date.now()}-${Math.random().toString(16).slice(2)}`; + }, + }; +} + diff --git a/examples/vanilla-js/backend/runtime/feature.js b/examples/vanilla-js/backend/runtime/feature.js new file mode 100644 index 0000000..b6ce344 --- /dev/null +++ b/examples/vanilla-js/backend/runtime/feature.js @@ -0,0 +1,38 @@ +export function defineFeature(definition) { + const requiredFields = ["name", "description", "input", "output", "capabilities", "execute"]; + + for (const field of requiredFields) { + if (!(field in definition)) { + throw new Error(`Feature is missing required field: ${field}`); + } + } + + if (!Array.isArray(definition.capabilities)) { + throw new Error("Feature capabilities must be an array"); + } + + if (typeof definition.execute !== "function") { + throw new Error("Feature execute must be a function"); + } + + return Object.freeze({ ...definition }); +} + +export function ok(value) { + return { + ok: true, + value, + }; +} + +export function fail(code, message, details = {}) { + return { + ok: false, + error: { + code, + message, + details, + }, + }; +} + diff --git a/examples/vanilla-js/backend/server.js b/examples/vanilla-js/backend/server.js new file mode 100644 index 0000000..56467cb --- /dev/null +++ b/examples/vanilla-js/backend/server.js @@ -0,0 +1,136 @@ +import { createServer } from "node:http"; +import { readFile } from "node:fs/promises"; +import { extname, join } from "node:path"; +import { fileURLToPath } from "node:url"; + +import { completeTask } from "./features/completeTask.js"; +import { createTask } from "./features/createTask.js"; +import { deleteTask } from "./features/deleteTask.js"; +import { listTasks } from "./features/listTasks.js"; +import { reopenTask } from "./features/reopenTask.js"; +import { createAifaRuntime } from "./runtime/aifaRuntime.js"; +import { createFileTaskStore } from "./storage/fileTaskStore.js"; + +const rootDir = join(fileURLToPath(new URL(".", import.meta.url)), ".."); +const frontendDir = join(rootDir, "frontend"); +const dataPath = join(rootDir, "data", "tasks.json"); + +const runtime = createAifaRuntime({ + taskStore: createFileTaskStore(dataPath), +}); + +const contentTypes = { + ".html": "text/html; charset=utf-8", + ".css": "text/css; charset=utf-8", + ".js": "text/javascript; charset=utf-8", + ".json": "application/json; charset=utf-8", +}; + +const server = createServer(async (request, response) => { + try { + const url = new URL(request.url, `http://${request.headers.host}`); + + if (url.pathname.startsWith("/api/")) { + await handleApi(request, response, url); + return; + } + + await serveStatic(response, url.pathname); + } catch (error) { + sendJson(response, 500, { + ok: false, + error: { + code: "server_error", + message: error.message, + }, + }); + } +}); + +async function handleApi(request, response, url) { + const actor = { + id: request.headers["x-demo-user"] ?? "demo-user", + }; + + if (request.method === "GET" && url.pathname === "/api/tasks") { + sendFeatureResult(response, await runtime.run(listTasks, {}, actor)); + return; + } + + if (request.method === "POST" && url.pathname === "/api/tasks") { + const body = await readJson(request); + sendFeatureResult(response, await runtime.run(createTask, { title: body.title }, actor), 201); + return; + } + + const taskActionMatch = url.pathname.match(/^\/api\/tasks\/([^/]+)\/(complete|reopen)$/); + + if (request.method === "POST" && taskActionMatch) { + const [, taskId, action] = taskActionMatch; + const feature = action === "complete" ? completeTask : reopenTask; + sendFeatureResult(response, await runtime.run(feature, { taskId }, actor)); + return; + } + + const taskDeleteMatch = url.pathname.match(/^\/api\/tasks\/([^/]+)$/); + + if (request.method === "DELETE" && taskDeleteMatch) { + const [, taskId] = taskDeleteMatch; + sendFeatureResult(response, await runtime.run(deleteTask, { taskId }, actor)); + return; + } + + sendJson(response, 404, { + ok: false, + error: { + code: "route_not_found", + message: "API route was not found", + }, + }); +} + +async function serveStatic(response, pathname) { + const requestedFile = pathname === "/" ? "index.html" : pathname.slice(1); + const filePath = join(frontendDir, requestedFile); + const content = await readFile(filePath); + const type = contentTypes[extname(filePath)] ?? "application/octet-stream"; + + response.writeHead(200, { "content-type": type }); + response.end(content); +} + +async function readJson(request) { + const chunks = []; + + for await (const chunk of request) { + chunks.push(chunk); + } + + if (chunks.length === 0) { + return {}; + } + + return JSON.parse(Buffer.concat(chunks).toString("utf8")); +} + +function sendFeatureResult(response, result, successStatus = 200) { + if (result.ok) { + sendJson(response, successStatus, result); + return; + } + + const status = result.error.code === "not_found" ? 404 : 400; + sendJson(response, status, result); +} + +function sendJson(response, status, payload) { + response.writeHead(status, { "content-type": "application/json; charset=utf-8" }); + response.end(JSON.stringify(payload, null, 2)); +} + +const port = Number(process.env.PORT ?? 3000); +const host = process.env.HOST ?? "127.0.0.1"; + +server.listen(port, host, () => { + console.log(`AIFA Task Board running at http://${host}:${port}`); +}); diff --git a/examples/vanilla-js/backend/storage/fileTaskStore.js b/examples/vanilla-js/backend/storage/fileTaskStore.js new file mode 100644 index 0000000..3334d8b --- /dev/null +++ b/examples/vanilla-js/backend/storage/fileTaskStore.js @@ -0,0 +1,50 @@ +import { mkdir, readFile, writeFile } from "node:fs/promises"; +import { dirname } from "node:path"; + +export function createFileTaskStore(filePath) { + async function readTasks() { + try { + const raw = await readFile(filePath, "utf8"); + return JSON.parse(raw); + } catch (error) { + if (error.code === "ENOENT") { + return []; + } + + throw error; + } + } + + async function writeTasks(tasks) { + await mkdir(dirname(filePath), { recursive: true }); + await writeFile(filePath, JSON.stringify(tasks, null, 2)); + } + + return { + async create(task) { + const tasks = await readTasks(); + await writeTasks([...tasks, task]); + return task; + }, + async load(taskId) { + const tasks = await readTasks(); + return tasks.find((task) => task.id === taskId) ?? null; + }, + async save(task) { + const tasks = await readTasks(); + await writeTasks(tasks.map((storedTask) => (storedTask.id === task.id ? task : storedTask))); + return task; + }, + async delete(taskId) { + const tasks = await readTasks(); + const nextTasks = tasks.filter((task) => task.id !== taskId); + await writeTasks(nextTasks); + return nextTasks.length !== tasks.length; + }, + async list() { + const tasks = await readTasks(); + return tasks.sort((left, right) => left.createdAt.localeCompare(right.createdAt)); + }, + }; +} + diff --git a/examples/vanilla-js/backend/storage/memoryTaskStore.js b/examples/vanilla-js/backend/storage/memoryTaskStore.js new file mode 100644 index 0000000..16793ce --- /dev/null +++ b/examples/vanilla-js/backend/storage/memoryTaskStore.js @@ -0,0 +1,24 @@ +export function createMemoryTaskStore(seed = []) { + const tasks = new Map(seed.map((task) => [task.id, task])); + + return { + async create(task) { + tasks.set(task.id, task); + return task; + }, + async load(taskId) { + return tasks.get(taskId) ?? null; + }, + async save(task) { + tasks.set(task.id, task); + return task; + }, + async delete(taskId) { + return tasks.delete(taskId); + }, + async list() { + return [...tasks.values()].sort((left, right) => left.createdAt.localeCompare(right.createdAt)); + }, + }; +} + diff --git a/examples/vanilla-js/examples/tickets/complete-task.md b/examples/vanilla-js/examples/tickets/complete-task.md new file mode 100644 index 0000000..d140612 --- /dev/null +++ b/examples/vanilla-js/examples/tickets/complete-task.md @@ -0,0 +1,38 @@ +# Feature: Complete Task + +## Goal + +Allow an actor to complete one existing task. + +## Input + +- `taskId`: string - The task to complete. + +## Output + +- `task`: Task - The completed task. + +## Allowed Capabilities + +- `clock.now` - Timestamp completion. +- `task.load` - Load the existing task. +- `task.save` - Persist the completed task. +- `audit.record` - Record the completion event. + +## Rules + +- The task must exist. +- Completing an already completed task succeeds without changing it. +- Completing an active task sets `completed` to `true`. +- Completing an active task sets `completedAt`. + +## Failure Cases + +- `not_found` - The task does not exist. + +## Acceptance Tests + +- Completes an active task. +- Returns success for an already completed task. +- Returns `not_found` for a missing task. + diff --git a/examples/vanilla-js/examples/tickets/create-task.md b/examples/vanilla-js/examples/tickets/create-task.md new file mode 100644 index 0000000..9c461da --- /dev/null +++ b/examples/vanilla-js/examples/tickets/create-task.md @@ -0,0 +1,37 @@ +# Feature: Create Task + +## Goal + +Allow an actor to create one active task. + +## Input + +- `title`: string - The task title. + +## Output + +- `task`: Task - The created task. + +## Allowed Capabilities + +- `id.create` - Generate a task id. +- `clock.now` - Timestamp the task. +- `task.create` - Persist the task. +- `audit.record` - Record the creation event. + +## Rules + +- The title must not be empty after trimming. +- New tasks start as incomplete. +- New tasks have no completion timestamp. + +## Failure Cases + +- `invalid_title` - The title is empty. + +## Acceptance Tests + +- Creates an active task. +- Trims the provided title. +- Rejects an empty title. + diff --git a/examples/vanilla-js/examples/tickets/delete-task.md b/examples/vanilla-js/examples/tickets/delete-task.md new file mode 100644 index 0000000..91173f6 --- /dev/null +++ b/examples/vanilla-js/examples/tickets/delete-task.md @@ -0,0 +1,34 @@ +# Feature: Delete Task + +## Goal + +Allow an actor to delete one existing task. + +## Input + +- `taskId`: string - The task to delete. + +## Output + +- `deleted`: boolean - Always `true` on success. + +## Allowed Capabilities + +- `task.load` - Confirm the task exists. +- `task.delete` - Delete the task. +- `audit.record` - Record the delete event. + +## Rules + +- The task must exist. +- Deleting a task removes it from subsequent list results. + +## Failure Cases + +- `not_found` - The task does not exist. + +## Acceptance Tests + +- Deletes an existing task. +- Returns `not_found` for a missing task. + diff --git a/examples/vanilla-js/examples/tickets/reopen-task.md b/examples/vanilla-js/examples/tickets/reopen-task.md new file mode 100644 index 0000000..1e3a7d8 --- /dev/null +++ b/examples/vanilla-js/examples/tickets/reopen-task.md @@ -0,0 +1,37 @@ +# Feature: Reopen Task + +## Goal + +Allow an actor to reopen one completed task. + +## Input + +- `taskId`: string - The task to reopen. + +## Output + +- `task`: Task - The reopened task. + +## Allowed Capabilities + +- `task.load` - Load the existing task. +- `task.save` - Persist the reopened task. +- `audit.record` - Record the reopen event. + +## Rules + +- The task must exist. +- Reopening an active task succeeds without changing it. +- Reopening a completed task sets `completed` to `false`. +- Reopening a completed task clears `completedAt`. + +## Failure Cases + +- `not_found` - The task does not exist. + +## Acceptance Tests + +- Reopens a completed task. +- Returns success for an active task. +- Returns `not_found` for a missing task. + diff --git a/examples/vanilla-js/frontend/app.js b/examples/vanilla-js/frontend/app.js new file mode 100644 index 0000000..d0a2aae --- /dev/null +++ b/examples/vanilla-js/frontend/app.js @@ -0,0 +1,130 @@ +const taskList = document.querySelector("#task-list"); +const taskTemplate = document.querySelector("#task-template"); +const taskForm = document.querySelector("#task-form"); +const taskTitle = document.querySelector("#task-title"); +const status = document.querySelector("#status"); +const traceCard = document.querySelector("#trace-card"); +const filterButtons = document.querySelectorAll(".filter"); + +let tasks = []; +let activeFilter = "all"; + +const traceCopy = { + "list-tasks": "UI asks for task state. API runs listTasks. Runtime provides task.list.", + "create-task": "UI sends a title. API runs createTask. Runtime provides id, clock, storage, and audit.", + "complete-task": "UI toggles a task. API runs completeTask. Runtime loads, saves, timestamps, and audits.", + "reopen-task": "UI toggles a done task. API runs reopenTask. Runtime loads, saves, and audits.", + "delete-task": "UI deletes a task. API runs deleteTask. Runtime loads, deletes, and audits.", +}; + +taskForm.addEventListener("submit", async (event) => { + event.preventDefault(); + const title = taskTitle.value; + + if (!title.trim()) { + setStatus("Title required"); + return; + } + + await api("/api/tasks", { + method: "POST", + body: JSON.stringify({ title }), + }); + + taskTitle.value = ""; + setTrace("create-task"); + await loadTasks(); +}); + +filterButtons.forEach((button) => { + button.addEventListener("click", () => { + activeFilter = button.dataset.filter; + filterButtons.forEach((candidate) => candidate.classList.toggle("active", candidate === button)); + render(); + }); +}); + +async function loadTasks() { + const result = await api("/api/tasks"); + tasks = result.value.tasks; + setTrace("list-tasks"); + render(); +} + +function render() { + taskList.replaceChildren(); + const visibleTasks = tasks.filter((task) => { + if (activeFilter === "active") { + return !task.completed; + } + + if (activeFilter === "completed") { + return task.completed; + } + + return true; + }); + + if (visibleTasks.length === 0) { + const empty = document.createElement("li"); + empty.className = "task-item"; + empty.textContent = "No tasks in this view."; + taskList.append(empty); + return; + } + + for (const task of visibleTasks) { + const item = taskTemplate.content.firstElementChild.cloneNode(true); + item.classList.toggle("completed", task.completed); + item.querySelector(".task-title").textContent = task.title; + + item.querySelector(".toggle").addEventListener("click", async () => { + const action = task.completed ? "reopen" : "complete"; + await api(`/api/tasks/${task.id}/${action}`, { method: "POST" }); + setTrace(task.completed ? "reopen-task" : "complete-task"); + await loadTasks(); + }); + + item.querySelector(".delete").addEventListener("click", async () => { + await api(`/api/tasks/${task.id}`, { method: "DELETE" }); + setTrace("delete-task"); + await loadTasks(); + }); + + taskList.append(item); + } + + setStatus(`${tasks.length} task${tasks.length === 1 ? "" : "s"}`); +} + +async function api(path, options = {}) { + const response = await fetch(path, { + headers: { + "content-type": "application/json", + ...options.headers, + }, + ...options, + }); + const result = await response.json(); + + if (!response.ok || !result.ok) { + setStatus(result.error?.message ?? "Request failed"); + throw new Error(result.error?.message ?? "Request failed"); + } + + return result; +} + +function setTrace(featureName) { + traceCard.querySelector("strong").textContent = featureName; + traceCard.querySelector("p").textContent = traceCopy[featureName]; +} + +function setStatus(message) { + status.textContent = message; +} + +loadTasks().catch((error) => { + setStatus(error.message); +}); + diff --git a/examples/vanilla-js/frontend/index.html b/examples/vanilla-js/frontend/index.html new file mode 100644 index 0000000..5492572 --- /dev/null +++ b/examples/vanilla-js/frontend/index.html @@ -0,0 +1,63 @@ + + + + + + AIFA Task Board + + + +
    +
    +
    +
    +

    Full-stack AIFA example

    +

    Task Board

    +
    +
    Runtime ready
    +
    + +
    + + +
    + +
    + + + +
    + +
      +
      + + +
      + + + + + + + diff --git a/examples/vanilla-js/frontend/styles.css b/examples/vanilla-js/frontend/styles.css new file mode 100644 index 0000000..5395d82 --- /dev/null +++ b/examples/vanilla-js/frontend/styles.css @@ -0,0 +1,280 @@ +:root { + --ink: #121a17; + --muted: #65746e; + --paper: #f7f7f2; + --panel: #ffffff; + --line: #dce6df; + --mint: #2ec49a; + --teal: #117f8c; + --coral: #f26b5b; +} + +* { + box-sizing: border-box; +} + +body { + margin: 0; + min-height: 100vh; + background: + radial-gradient(circle at 20% 10%, rgba(46, 196, 154, 0.18), transparent 28%), + radial-gradient(circle at 90% 80%, rgba(242, 107, 91, 0.16), transparent 26%), + var(--paper); + color: var(--ink); + font-family: + Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", + sans-serif; +} + +button, +input { + font: inherit; +} + +.app-shell { + display: grid; + gap: 24px; + grid-template-columns: minmax(0, 1fr) 360px; + min-height: 100vh; + padding: 28px; +} + +.workspace, +.architecture-panel { + background: rgba(255, 255, 255, 0.82); + border: 1px solid var(--line); + border-radius: 8px; + box-shadow: 0 24px 70px rgba(18, 26, 23, 0.1); +} + +.workspace { + padding: clamp(24px, 4vw, 52px); +} + +.workspace-header { + align-items: center; + display: flex; + gap: 18px; + justify-content: space-between; + margin-bottom: 28px; +} + +.eyebrow { + color: var(--teal); + font-size: 12px; + font-weight: 900; + letter-spacing: 0; + margin: 0 0 8px; + text-transform: uppercase; +} + +h1, +h2, +p { + margin-top: 0; +} + +h1 { + font-size: clamp(46px, 8vw, 92px); + letter-spacing: 0; + line-height: 0.95; + margin-bottom: 0; +} + +h2 { + font-size: 32px; + letter-spacing: 0; + line-height: 1.05; +} + +.status-pill { + background: #12221c; + border-radius: 8px; + color: #effaf6; + font-size: 14px; + font-weight: 850; + padding: 10px 12px; +} + +.task-form { + display: grid; + gap: 12px; + grid-template-columns: 1fr auto; + margin-bottom: 18px; +} + +.task-form input { + background: var(--panel); + border: 1px solid var(--line); + border-radius: 8px; + color: var(--ink); + min-height: 54px; + padding: 0 16px; +} + +.task-form input:focus { + border-color: var(--mint); + outline: 3px solid rgba(46, 196, 154, 0.18); +} + +.task-form button, +.filter, +.delete { + border: 0; + border-radius: 8px; + cursor: pointer; + font-weight: 850; +} + +.task-form button { + background: var(--ink); + color: var(--paper); + min-height: 54px; + padding: 0 22px; +} + +.task-toolbar { + display: flex; + gap: 8px; + margin-bottom: 18px; +} + +.filter { + background: transparent; + border: 1px solid var(--line); + color: var(--muted); + padding: 9px 12px; +} + +.filter.active { + background: rgba(46, 196, 154, 0.12); + border-color: rgba(46, 196, 154, 0.6); + color: var(--ink); +} + +.task-list { + display: grid; + gap: 10px; + list-style: none; + margin: 0; + padding: 0; +} + +.task-item { + align-items: center; + background: var(--panel); + border: 1px solid var(--line); + border-radius: 8px; + display: grid; + gap: 12px; + grid-template-columns: auto 1fr auto; + min-height: 64px; + padding: 12px; +} + +.task-item.completed .task-title { + color: var(--muted); + text-decoration: line-through; +} + +.toggle { + background: transparent; + border: 2px solid var(--line); + border-radius: 8px; + cursor: pointer; + height: 30px; + width: 30px; +} + +.task-item.completed .toggle { + background: var(--mint); + border-color: var(--mint); +} + +.task-title { + font-size: 18px; + font-weight: 750; +} + +.delete { + background: #fff0ee; + color: #a5382c; + padding: 9px 11px; +} + +.architecture-panel { + align-self: start; + min-height: calc(100vh - 56px); + padding: 28px; + position: sticky; + top: 28px; +} + +.architecture-panel p { + color: var(--muted); +} + +.trace-card { + background: #12221c; + border-radius: 8px; + color: var(--paper); + margin: 28px 0; + padding: 20px; +} + +.trace-card span { + color: #9ddfce; + display: block; + font-size: 12px; + font-weight: 900; + margin-bottom: 10px; + text-transform: uppercase; +} + +.trace-card strong { + display: block; + font-size: 28px; + line-height: 1; + margin-bottom: 12px; +} + +.trace-card p { + color: #d6e8e1; + margin-bottom: 0; +} + +.flow { + display: grid; + gap: 10px; +} + +.flow div { + background: rgba(46, 196, 154, 0.1); + border: 1px solid rgba(46, 196, 154, 0.28); + border-radius: 8px; + font-weight: 850; + padding: 12px; +} + +@media (max-width: 900px) { + .app-shell { + grid-template-columns: 1fr; + padding: 16px; + } + + .workspace-header, + .task-form { + grid-template-columns: 1fr; + } + + .workspace-header { + align-items: flex-start; + flex-direction: column; + } + + .architecture-panel { + min-height: 0; + position: static; + } +} + diff --git a/examples/vanilla-js/package.json b/examples/vanilla-js/package.json new file mode 100644 index 0000000..994805f --- /dev/null +++ b/examples/vanilla-js/package.json @@ -0,0 +1,15 @@ +{ + "name": "aifa-fullstack-task-board", + "version": "0.1.0", + "description": "A full-stack example app built with AI Atomic Feature Architecture.", + "type": "module", + "private": true, + "scripts": { + "start": "node backend/server.js", + "test": "node --test" + }, + "engines": { + "node": ">=20" + } +} + diff --git a/examples/vanilla-js/test/taskFeatures.test.js b/examples/vanilla-js/test/taskFeatures.test.js new file mode 100644 index 0000000..5bccdfb --- /dev/null +++ b/examples/vanilla-js/test/taskFeatures.test.js @@ -0,0 +1,130 @@ +import assert from "node:assert/strict"; +import test from "node:test"; + +import { completeTask } from "../backend/features/completeTask.js"; +import { createTask } from "../backend/features/createTask.js"; +import { deleteTask } from "../backend/features/deleteTask.js"; +import { listTasks } from "../backend/features/listTasks.js"; +import { reopenTask } from "../backend/features/reopenTask.js"; +import { createAifaRuntime } from "../backend/runtime/aifaRuntime.js"; +import { createMemoryTaskStore } from "../backend/storage/memoryTaskStore.js"; + +const actor = { + id: "test-user", +}; + +function createRuntime(seed = []) { + let nextId = 1; + + return createAifaRuntime({ + taskStore: createMemoryTaskStore(seed), + idFactory: { + next() { + const id = `task-${nextId}`; + nextId += 1; + return id; + }, + }, + clock: () => new Date("2026-08-14T10:00:00.000Z"), + }); +} + +test("creates an active task", async () => { + const runtime = createRuntime(); + + const result = await runtime.run(createTask, { title: " Write AIFA docs " }, actor); + + assert.equal(result.ok, true); + assert.equal(result.value.task.id, "task-1"); + assert.equal(result.value.task.title, "Write AIFA docs"); + assert.equal(result.value.task.completed, false); + assert.equal(result.value.task.completedAt, null); + assert.equal(runtime.auditLog[0].type, "task.created"); +}); + +test("rejects empty task titles", async () => { + const runtime = createRuntime(); + + const result = await runtime.run(createTask, { title: " " }, actor); + + assert.equal(result.ok, false); + assert.equal(result.error.code, "invalid_title"); +}); + +test("lists tasks in creation order", async () => { + const runtime = createRuntime([ + createSeedTask("task-2", "Second", "2026-08-14T10:02:00.000Z"), + createSeedTask("task-1", "First", "2026-08-14T10:01:00.000Z"), + ]); + + const result = await runtime.run(listTasks, {}, actor); + + assert.equal(result.ok, true); + assert.deepEqual( + result.value.tasks.map((task) => task.id), + ["task-1", "task-2"], + ); +}); + +test("completes an active task", async () => { + const runtime = createRuntime([createSeedTask("task-1", "Ship example")]); + + const result = await runtime.run(completeTask, { taskId: "task-1" }, actor); + + assert.equal(result.ok, true); + assert.equal(result.value.task.completed, true); + assert.equal(result.value.task.completedAt, "2026-08-14T10:00:00.000Z"); + assert.equal(runtime.auditLog[0].type, "task.completed"); +}); + +test("reopens a completed task", async () => { + const runtime = createRuntime([ + { + ...createSeedTask("task-1", "Revisit article"), + completed: true, + completedAt: "2026-08-14T09:00:00.000Z", + }, + ]); + + const result = await runtime.run(reopenTask, { taskId: "task-1" }, actor); + + assert.equal(result.ok, true); + assert.equal(result.value.task.completed, false); + assert.equal(result.value.task.completedAt, null); + assert.equal(runtime.auditLog[0].type, "task.reopened"); +}); + +test("deletes an existing task", async () => { + const runtime = createRuntime([createSeedTask("task-1", "Delete me")]); + + const result = await runtime.run(deleteTask, { taskId: "task-1" }, actor); + const listResult = await runtime.run(listTasks, {}, actor); + + assert.equal(result.ok, true); + assert.equal(result.value.deleted, true); + assert.equal(listResult.value.tasks.length, 0); + assert.equal(runtime.auditLog[0].type, "task.deleted"); +}); + +test("returns not_found for missing task actions", async () => { + const runtime = createRuntime(); + + const completeResult = await runtime.run(completeTask, { taskId: "missing" }, actor); + const reopenResult = await runtime.run(reopenTask, { taskId: "missing" }, actor); + const deleteResult = await runtime.run(deleteTask, { taskId: "missing" }, actor); + + assert.equal(completeResult.error.code, "not_found"); + assert.equal(reopenResult.error.code, "not_found"); + assert.equal(deleteResult.error.code, "not_found"); +}); + +function createSeedTask(id, title, createdAt = "2026-08-14T10:00:00.000Z") { + return { + id, + title, + completed: false, + createdAt, + completedAt: null, + }; +} + From 6ab55eff3978d3cf4c7c9449b4bbff868796ef21 Mon Sep 17 00:00:00 2001 From: Adrian Besleaga Date: Sat, 15 Aug 2026 10:03:01 +0200 Subject: [PATCH 2/6] docs(ai-todo-assistant): define AIFA enterprise architecture and contract tooling --- .gitignore | 3 + demo/ai-todo-assistant/AGENTS.md | 123 ++ demo/ai-todo-assistant/ARCHITECTURE.md | 210 +++ demo/ai-todo-assistant/BUSINESS.md | 49 + .../events/v1/task-plan-generated.schema.json | 1 + .../v1/task-plan-suggestion.schema.json | 14 + .../generate-task-plan/IMPLEMENTATION_PLAN.md | 39 + .../contracts/input.schema.json | 1 + .../contracts/output.schema.json | 1 + .../feature.definition.json | 18 + .../events/v1/task-created.schema.json | 1 + .../events/v1/task-deleted.schema.json | 1 + .../events/v1/task-status-changed.schema.json | 1 + .../contracts/v1/task-taxonomy.schema.json | 13 + .../contracts/v1/task-view.schema.json | 19 + .../contracts/v1/task.schema.json | 20 + .../change-task-status/IMPLEMENTATION_PLAN.md | 43 + .../contracts/input.schema.json | 1 + .../contracts/output.schema.json | 1 + .../feature.definition.json | 18 + .../create-task/IMPLEMENTATION_PLAN.md | 39 + .../create-task/contracts/input.schema.json | 1 + .../create-task/contracts/output.schema.json | 1 + .../create-task/feature.definition.json | 18 + .../delete-task/IMPLEMENTATION_PLAN.md | 38 + .../delete-task/contracts/input.schema.json | 1 + .../delete-task/contracts/output.schema.json | 1 + .../delete-task/feature.definition.json | 17 + .../list-tasks/IMPLEMENTATION_PLAN.md | 38 + .../list-tasks/contracts/input.schema.json | 1 + .../list-tasks/contracts/output.schema.json | 1 + .../list-tasks/feature.definition.json | 18 + .../core/IMPLEMENTATION_PLAN.md | 129 ++ .../core/architecture/.dependency-cruiser.cjs | 12 + .../core/architecture/dependency-policy.json | 16 + .../feature-definition.schema.json | 82 + .../architecture/generate-contract-types.ts | 43 + .../validate-feature-definitions.ts | 112 ++ .../core/shared/architecture-enums.ts | 41 + demo/ai-todo-assistant/package-lock.json | 1419 +++++++++++++++++ demo/ai-todo-assistant/package.json | 22 + demo/ai-todo-assistant/tsconfig.json | 13 + 42 files changed, 2640 insertions(+) create mode 100644 demo/ai-todo-assistant/AGENTS.md create mode 100644 demo/ai-todo-assistant/ARCHITECTURE.md create mode 100644 demo/ai-todo-assistant/BUSINESS.md create mode 100644 demo/ai-todo-assistant/contexts/ai-planning/contracts/events/v1/task-plan-generated.schema.json create mode 100644 demo/ai-todo-assistant/contexts/ai-planning/contracts/v1/task-plan-suggestion.schema.json create mode 100644 demo/ai-todo-assistant/contexts/ai-planning/features/generate-task-plan/IMPLEMENTATION_PLAN.md create mode 100644 demo/ai-todo-assistant/contexts/ai-planning/features/generate-task-plan/contracts/input.schema.json create mode 100644 demo/ai-todo-assistant/contexts/ai-planning/features/generate-task-plan/contracts/output.schema.json create mode 100644 demo/ai-todo-assistant/contexts/ai-planning/features/generate-task-plan/feature.definition.json create mode 100644 demo/ai-todo-assistant/contexts/task-management/contracts/events/v1/task-created.schema.json create mode 100644 demo/ai-todo-assistant/contexts/task-management/contracts/events/v1/task-deleted.schema.json create mode 100644 demo/ai-todo-assistant/contexts/task-management/contracts/events/v1/task-status-changed.schema.json create mode 100644 demo/ai-todo-assistant/contexts/task-management/contracts/v1/task-taxonomy.schema.json create mode 100644 demo/ai-todo-assistant/contexts/task-management/contracts/v1/task-view.schema.json create mode 100644 demo/ai-todo-assistant/contexts/task-management/contracts/v1/task.schema.json create mode 100644 demo/ai-todo-assistant/contexts/task-management/features/change-task-status/IMPLEMENTATION_PLAN.md create mode 100644 demo/ai-todo-assistant/contexts/task-management/features/change-task-status/contracts/input.schema.json create mode 100644 demo/ai-todo-assistant/contexts/task-management/features/change-task-status/contracts/output.schema.json create mode 100644 demo/ai-todo-assistant/contexts/task-management/features/change-task-status/feature.definition.json create mode 100644 demo/ai-todo-assistant/contexts/task-management/features/create-task/IMPLEMENTATION_PLAN.md create mode 100644 demo/ai-todo-assistant/contexts/task-management/features/create-task/contracts/input.schema.json create mode 100644 demo/ai-todo-assistant/contexts/task-management/features/create-task/contracts/output.schema.json create mode 100644 demo/ai-todo-assistant/contexts/task-management/features/create-task/feature.definition.json create mode 100644 demo/ai-todo-assistant/contexts/task-management/features/delete-task/IMPLEMENTATION_PLAN.md create mode 100644 demo/ai-todo-assistant/contexts/task-management/features/delete-task/contracts/input.schema.json create mode 100644 demo/ai-todo-assistant/contexts/task-management/features/delete-task/contracts/output.schema.json create mode 100644 demo/ai-todo-assistant/contexts/task-management/features/delete-task/feature.definition.json create mode 100644 demo/ai-todo-assistant/contexts/task-management/features/list-tasks/IMPLEMENTATION_PLAN.md create mode 100644 demo/ai-todo-assistant/contexts/task-management/features/list-tasks/contracts/input.schema.json create mode 100644 demo/ai-todo-assistant/contexts/task-management/features/list-tasks/contracts/output.schema.json create mode 100644 demo/ai-todo-assistant/contexts/task-management/features/list-tasks/feature.definition.json create mode 100644 demo/ai-todo-assistant/core/IMPLEMENTATION_PLAN.md create mode 100644 demo/ai-todo-assistant/core/architecture/.dependency-cruiser.cjs create mode 100644 demo/ai-todo-assistant/core/architecture/dependency-policy.json create mode 100644 demo/ai-todo-assistant/core/architecture/feature-definition.schema.json create mode 100644 demo/ai-todo-assistant/core/architecture/generate-contract-types.ts create mode 100644 demo/ai-todo-assistant/core/architecture/validate-feature-definitions.ts create mode 100644 demo/ai-todo-assistant/core/shared/architecture-enums.ts create mode 100644 demo/ai-todo-assistant/package-lock.json create mode 100644 demo/ai-todo-assistant/package.json create mode 100644 demo/ai-todo-assistant/tsconfig.json diff --git a/.gitignore b/.gitignore index bbc1822..cf5ff70 100644 --- a/.gitignore +++ b/.gitignore @@ -7,6 +7,9 @@ dist/ # Local runtime data for the React + Node example examples/aifa-react-node-slots/data/tasks.json +# Generated contract declarations +demo/ai-todo-assistant/core/shared/generated/ + # Test coverage coverage/ diff --git a/demo/ai-todo-assistant/AGENTS.md b/demo/ai-todo-assistant/AGENTS.md new file mode 100644 index 0000000..c989d39 --- /dev/null +++ b/demo/ai-todo-assistant/AGENTS.md @@ -0,0 +1,123 @@ +# AI Todo Assistant: AIFA Working Agreement + +> **Required reading:** Follow [ARCHITECTURE.md](ARCHITECTURE.md) before adding or changing any code, feature definition, domain rule, capability, route, MCP tool, or slot contribution. It is the authoritative architecture document; this file is the contributor working agreement. + +## Purpose + +This app is a production-style demonstration of **AI Atomic Feature Architecture (AIFA)**. + +AIFA treats a software feature as a small, explicit, independently implementable contract. A feature receives one context object containing its input, actor, metadata, and explicitly granted capabilities. It must not import infrastructure directly. The runtime owns infrastructure such as databases, authentication, logging, and transactions. + +The goal is to make a feature understandable and safely changeable without requiring a contributor to learn the entire codebase first. + +## Product Goal + +Build a category-based AI todo assistant that demonstrates a full application can be assembled from small, isolated feature slices. Users describe what they want to achieve; AI proposes categorized tasks and can move their status through governed product operations. The architecture should feel like a puzzle: + +- each piece has a clear purpose and contract; +- pieces can be developed and tested independently; +- pieces compose through explicit backend capabilities and frontend slots; +- humans and AI agents can work in parallel with minimal overlap and merge conflict; +- no feature relies on hidden conventions or direct infrastructure imports. + +## Required Stack + +- Node.js +- TypeScript +- MongoDB +- React +- Typed React slots for frontend extension points +- Material UI (MUI) for visual components and layout +- Model Context Protocol (MCP) server for external AI clients +- Ollama support through a provider-neutral AI capability + +Use MUI components and its theme system. Do not create a bespoke CSS component system or add custom stylesheet-driven UI components. Small layout props and theme configuration are acceptable. + +## Architecture Rules + +### Enums over unstructured identifiers + +Use TypeScript `enum`s for stable, closed sets of internal identifiers instead of repeating plain strings. This includes feature names, capability names, slot names, task categories/priorities/statuses, API error codes, and audit event types. + +Do not introduce an enum for arbitrary user input, database IDs, free-form text, or values that must remain extensible. Convert external strings at the application boundary into the relevant enum before feature logic runs. + +### Dynamic feature registration + +Features register themselves through a feature-local manifest; core discovers and composes those manifests dynamically. + +- Each feature exports one manifest from its own folder. +- A backend manifest declares its AIFA feature, route metadata, and capability requirements. +- A frontend manifest declares its slot contributions. +- An MCP manifest declares its feature-local tool metadata when the feature is safe to expose to MCP clients. +- Core contains generic discovery/registration mechanisms only; it must not maintain a hand-edited list of product features. +- Adding a feature should mean adding one feature folder, with no changes to a central registry unless a new shared capability or slot contract is genuinely required. + +Use build-tool-supported module discovery on the frontend and a filesystem/module discovery adapter on the backend. Keep discovery deterministic and fail clearly for duplicate feature names, routes, or slot contribution names. + +MCP tools must invoke the same discovered AIFA features as HTTP routes. They must never bypass feature validation, runtime capabilities, or auditing. + +### Bounded contexts and feature slices + +Product business rules live in bounded contexts under `contexts//`. Each context may contain `domain/` for context-specific entities, value objects, and invariants, plus `features//` for application use cases. + +This is the one intentional exception to the rule that implementation stays in a feature folder: shared business invariants belong to their bounded context, never in `core/` and never duplicated across features. + +Each feature folder contains both: + +- `backend/` — its AIFA feature definition, input/output contract, and feature-local tests. +- `frontend/` — its React slot contribution, view model, and feature-local UI tests where appropriate. + +Examples: `contexts/task-management/features/create-task` and `contexts/ai-planning/features/generate-task-plan`. + +A feature folder must include `feature.definition.json`. It is validated against `core/architecture/feature-definition.schema.json` and declares its business need, dependencies, consumed contracts, emitted events, route, capabilities, MCP exposure, slots, and implementation work items. + +### Core + +Only cross-cutting infrastructure belongs in `core/`: + +- the AIFA runtime and shared result/feature types; +- MongoDB connection and capability adapters; +- HTTP server and route composition; +- shared domain types; +- React app shell, API transport, MUI theme, and slot registry. + +Core must not contain product behavior that belongs to a feature slice. + +### Dependency rules + +- `core/` may not import a context or feature. +- A feature may import `core/`, its own bounded context's `domain/`, and itself only. +- A context may not import another context's domain or features directly. +- Cross-context work uses versioned contracts from `contexts//contracts/v/` or domain events. +- Feature `dependsOn` declarations must form a directed acyclic graph. +- `dependsOn` may name only a feature whose published contract or persisted lifecycle artifact is required; it never authorizes imports or direct feature calls. +- CI runs JSON Schema validation, graph-cycle validation, and dependency-cruiser import rules before merge. + +### Backend boundaries + +- A feature may use only `context.input`, `context.actor`, `context.metadata`, `context.capabilities`, `context.ok`, and `context.fail`. +- Features must declare the capabilities they require. +- Features must not import MongoDB clients, HTTP objects, environment variables, or other feature implementations. +- Routes adapt HTTP to one feature invocation; they do not contain business logic. +- MongoDB access is exposed through runtime capabilities, never imported by a feature. +- Features must never receive, set, or infer a tenant identifier; runtime derives tenancy from the authenticated actor. +- Every mutation declares idempotency, actor, scope, and confirmation requirements in `feature.definition.json`. + +### Frontend boundaries + +- The app shell exposes named, typed slots and owns composition only. +- A feature renders UI by registering a contribution to a named slot. +- A slot model is an explicit contract between the app shell and a contribution. +- Feature UI must not reach into other feature folders or depend on app-shell internals. + +## Contribution Checklist + +Before considering a feature complete: + +1. Define or update its explicit input, output, error, and capability contract. +2. Implement the backend behavior in the feature folder only. +3. Add its frontend contribution in the same feature folder. +4. Add focused behavioral tests that run the feature through the AIFA runtime. +5. Verify the feature can be understood without reading unrelated features. +6. Keep shared changes small and intentional; do not move feature behavior into `core/` for convenience. +7. Add or update authorization, tenant-isolation, idempotency, and concurrency tests for a mutation. diff --git a/demo/ai-todo-assistant/ARCHITECTURE.md b/demo/ai-todo-assistant/ARCHITECTURE.md new file mode 100644 index 0000000..aace84c --- /dev/null +++ b/demo/ai-todo-assistant/ARCHITECTURE.md @@ -0,0 +1,210 @@ +# AI Todo Assistant Architecture + +## Purpose + +AI Todo Assistant is a category-based task-management product. A person describes an outcome in natural language; an AI proposes categorized tasks and can update task status through the same governed operations available to the web application. + +The application demonstrates AI Atomic Feature Architecture (AIFA): software is built from small, explicit, independently understandable feature slices. This lets humans and AI agents work concurrently with minimal hidden context and merge conflict. + +See [BUSINESS.md](BUSINESS.md) for the customer problem and product goals. + +## Architectural Principles + +1. **A feature is a contract.** It declares input, output, failures, capabilities, HTTP/MCP exposure, frontend slots, and technical work items. +2. **Infrastructure is runtime-owned.** Features receive capabilities; they never import MongoDB, HTTP, environment configuration, AI SDKs, or other feature implementations. +3. **Business rules stay in their bounded context.** Core is platform infrastructure, never a dumping ground for task or AI-planning behavior. +4. **Composition is dynamic.** Feature definitions and manifests are discovered rather than registered in hand-edited central lists. +5. **Identifiers are typed.** Stable internal values use TypeScript enums, including feature names, capabilities, slots, categories, priorities, statuses, error codes, audit events, and HTTP methods. +6. **Every boundary is enforced.** Schema validation, dependency analysis, behavioral tests, and runtime capability checks run before a feature is accepted. + +## Technology Stack + +- Node.js and TypeScript +- MongoDB +- React and Material UI (MUI) +- Typed React slots +- Model Context Protocol (MCP) server +- Ollama through a provider-neutral AI capability + +MUI is the visual component system. Do not create a parallel custom CSS component library. + +## Structure + +```txt +demo/ai-todo-assistant/ + core/ # platform/composition only + shared/ # contracts, enums, result types + backend/ # runtime, MongoDB, HTTP, MCP, discovery + frontend/ # app shell, slots, API, theme, discovery + architecture/ # schema, dependency policy, validators + contexts/ + task-management/ + domain/ # Task invariants and value objects + features/ + create-task/ + list-tasks/ + change-task-status/ + delete-task/ + ai-planning/ + domain/ # AI-planning concepts and validation + features/ + generate-task-plan/ + BUSINESS.md + AGENTS.md + ARCHITECTURE.md +``` + +## Core + +`core/` owns generic composition and adapters only: + +- AIFA runtime and `FeatureContext` contracts; +- MongoDB client, repositories, audit/outbox infrastructure, and runtime capability adapters; +- dynamic backend route and MCP-tool registration; +- React shell, typed slot registry, Vite manifest discovery, MUI theme, and API transport; +- shared enum and contract definitions; +- architecture validators. + +Core must not implement task creation, status transitions, AI task planning, or any other product behavior. + +## Bounded Contexts + +A bounded context owns its business language, entities, value objects, and invariants. + +### Task Management + +Owns `Task`, `TaskCategory`, `TaskPriority`, and `TaskStatus`. Initial categories are Work, Personal, Sport, Shopping, and Other. Initial statuses are Todo, InProgress, and Completed. + +The task-status transition invariant lives here. Features invoke it; they do not duplicate it. + +`Task` also has an integer `version`. Every mutation requires the version observed by the caller and performs an optimistic-concurrency update. A stale write returns `ApiErrorCode.VersionConflict` with the current task representation. + +### AI Planning + +Owns the persisted `TaskPlan` aggregate, validated task-plan suggestions, and the rules for safely turning provider output into task categories and priorities. Generating a plan creates a tenant-owned review record and emits `TaskPlanGeneratedV1`; it does not create tasks. Accepted suggestions use the Create Task feature boundary. + +Contexts do not import each other’s domain or feature modules. They communicate using versioned contracts or domain events. + +`contexts//contracts/v/` is the published-language boundary. It contains JSON Schemas and generated TypeScript types that other contexts may consume. For example, AI Planning may consume `task-management/contracts/v1/task-taxonomy.schema.json`; it must not import Task Management’s domain module. Every cross-context contract is versioned, backward-compatible within its major version, and owned by its publishing context. + +## Security and Multi-Tenancy + +Every request—HTTP, MCP, browser, or provider callback—must resolve to an `Actor` containing a `TenantId`, `UserId`, and enum-backed permission scopes. Anonymous mutation is prohibited. + +Browser authentication uses an OpenID Connect authorization-code flow with PKCE. The remote MCP endpoint uses Streamable HTTP and acts as an OAuth 2.1 protected resource: it validates bearer-token audience, expiry, tenant, user, and scopes on every request. A local stdio MCP connection obtains its credentials from the local environment and is restricted to the configured tenant. Token revocation, client registration, and scope assignment are owned by the identity adapter in core. + +Tasks, task plans, audits, idempotency records, and outbox events are tenant-owned. MongoDB capability adapters receive the actor and apply `tenantId` to every query and mutation; the feature never supplies or overrides a tenant ID. Required indexes include `{ tenantId, status, category }`, `{ tenantId, createdAt }`, and unique `{ tenantId, actorId, featureName, idempotencyKey }` for commands. + +MongoDB must run as a replica set (or sharded cluster) in every environment that enables multi-document mutations, including integration tests. This is required for the task mutation, audit, idempotency record, and outbox event to commit atomically. + +MCP authentication maps each client connection to an actor and only exposes tools permitted by its scopes. Feature definitions declare whether a tool is exposed, its required scope enum, whether it mutates data, and whether it requires confirmation. Deletion always requires confirmation; status changes require an explicit user instruction. + +## Command Safety and Concurrency + +Every mutation has a caller-supplied idempotency key and expected task version where relevant. Runtime capabilities persist the command result with the idempotency record, so an HTTP or MCP retry returns the original result without repeating persistence or audit work. + +Features use typed failures for `VersionConflict`, `IdempotencyKeyReuse`, `NotAuthorized`, and `ConfirmationRequired`. These failures are part of each adapter contract, not raw infrastructure errors. + +## Events, Audit, and Outbox + +Within the MongoDB transaction for a successful mutation, the runtime writes the task change, audit event, and an outbox event. Initial versioned event contracts are: + +- `TaskCreatedV1` +- `TaskStatusChangedV1` +- `TaskDeletedV1` +- `TaskPlanGeneratedV1` + +An outbox publisher delivers events asynchronously and records delivery state. Consumers receive versioned event contracts, never direct imports from another context. This preserves reliability without coupling feature implementations. + +## Feature Slice Contract + +Each feature lives at `contexts//features//` and contains: + +```txt +/ + feature.definition.json # machine-readable technical work contract + manifest.ts # executable backend/frontend/MCP registration + backend/ # AIFA feature, mapper, focused tests + frontend/ # slot contribution, view model, focused tests + IMPLEMENTATION_PLAN.md # human-readable implementation guide +``` + +`feature.definition.json` is validated against `core/architecture/feature-definition.schema.json`. It includes: + +- business need; +- bounded context and feature name; +- feature dependencies; +- HTTP route and method; +- required capabilities; +- MCP exposure and tool name; +- frontend slots; +- mutation, actor, idempotency, confirmation, and MCP-scope requirements; +- technical work items with acceptance criteria. + +Each executable input/output/MCP-tool/event shape has a colocated JSON Schema. `npm run generate:contracts` generates TypeScript types from those schemas; adapters validate untrusted data with Ajv plus format support before it reaches a feature. A feature definition references its input and output schemas, preventing contracts from drifting between JSON, HTTP, MCP, and TypeScript. + +The definition is the planning contract; `manifest.ts` is the executable binding. Neither replaces the other. + +## Dynamic Registration + +Backend discovery scans `contexts/*/features/*/feature.definition.json`, validates definitions and the dependency graph, then imports each matching `manifest.ts` in deterministic enum order. + +`dependsOn` means a feature requires another feature’s **published, versioned contract or persisted lifecycle artifact** to be available; it never permits an import or direct feature call. `consumesContracts` names the exact public schemas involved. Both are correctly empty for independent task commands. They become non-empty, for example, if a future `approve-generated-plan` feature consumes the persisted `TaskPlanGeneratedV1` contract published by Generate Task Plan. + +Frontend discovery uses Vite `import.meta.glob` to load feature manifests and register typed slot contributions in deterministic order. + +MCP discovery loads only manifest-declared tools. Each tool calls the same AIFA feature as the HTTP route, so MCP clients cannot bypass validation, authorization, capability checks, or audit records. + +## Dependency Boundaries + +```txt +core ────────────────────────────► core only +context domain ──────────────────► core + its own domain +feature ─────────────────────────► core + its own domain + itself +adapter (HTTP/MCP/UI) ───────────► core + its registered feature contract +cross-context work ──────────────► versioned contracts or domain events only +``` + +Forbidden dependencies: + +- core → context or feature; +- feature → another feature; +- a context → another context’s domain or feature; +- direct MongoDB/HTTP/MCP/provider SDK access from a feature; +- circular imports. + +`npm run check:imports` runs dependency-cruiser in CI to enforce import boundaries and circular-import rules. `validate-feature-definitions.ts` validates feature JSON, enum references, contract files, routes, MCP tools, security invariants, and the declared dependency graph. + +## AIFA Runtime Rules + +Every feature receives only: + +- validated input; +- actor and metadata; +- explicitly declared capabilities; +- `ok` and `fail` result helpers. + +The runtime denies undeclared capabilities. MongoDB access, authorization, audit events, idempotency, and transaction/outbox behavior belong to capabilities and adapters, not features. + +## AI, Ollama, and MCP + +The Generate Task Plan feature calls `AssistantGenerateTaskPlan`, a provider-neutral runtime capability. The first adapter targets a locally configured Ollama instance. Future cloud providers implement the same adapter contract. + +MCP-compatible clients can create, list, and delete tasks; generate plans; and move task statuses. AI mutations use the same feature contracts as browser actions. Suggestions remain reviewable before task creation, while status changes are explicit and audited. + +Provider output must satisfy a versioned JSON Schema before it reaches feature logic. The Ollama adapter applies timeouts, bounded retries, rate limits, health checks, and response-size limits. Prompt text and provider responses are treated as untrusted data: they never select capabilities, bypass authorization, or directly mutate tasks. + +## Quality Gates + +Before merge, CI must run: + +1. TypeScript type checking. +2. JSON Schema validation for every feature definition. +3. Declared dependency DAG validation. +4. dependency-cruiser boundary and circular-import checks. +5. Feature behavioral tests through the AIFA runtime. +6. MongoDB integration tests using a disposable test database. +7. HTTP and MCP contract tests confirming they invoke the same feature behavior. +8. Authorization, tenant-isolation, idempotency, optimistic-concurrency, and outbox integration tests. + +These gates keep a growing enterprise product modular, independently testable, and safe for concurrent AI and human development. diff --git a/demo/ai-todo-assistant/BUSINESS.md b/demo/ai-todo-assistant/BUSINESS.md new file mode 100644 index 0000000..178220e --- /dev/null +++ b/demo/ai-todo-assistant/BUSINESS.md @@ -0,0 +1,49 @@ +# AI Todo Assistant: Business Goal + +## The Product + +AI Todo Assistant is a personal task-management product for people who know what they want to accomplish but do not want to spend time translating every intention into a structured todo list. + +Users organize their life by the initial system categories Work, Personal, Sport, Shopping, and Other. Instead of manually writing, prioritizing, and updating every task, a user can describe an outcome in natural language: + +> “Prepare everything for my half marathon next month.” + +The assistant turns that goal into useful, categorized tasks with priorities. It can also update ticket status as work progresses. + +## Business Problem + +Traditional todo apps make users do all administrative work: break down goals, decide categories, create tickets, set priorities, and keep statuses current. This friction means plans are incomplete or lists become stale. + +AI Todo Assistant reduces that overhead while preserving user control. The AI proposes work; the user reviews, accepts, changes, or deletes it. Every change is visible as an ordinary task operation, not an opaque AI side effect. + +## Product Goals + +- Convert natural-language goals into practical task plans. +- Organize tasks using `TaskCategory` values such as Work, Personal, Sport, Shopping, and Other. +- Track task state with explicit `TaskStatus` values such as Todo, InProgress, and Completed. +- Let AI assistants create tasks and move task status through the same governed product operations available to a human. +- Allow users to connect their preferred AI client through an MCP server, including locally hosted Ollama models. +- Keep sensitive task data under the user’s chosen deployment and model-provider configuration. + +Custom tenant-defined categories are deliberately out of scope for the first release: they cannot safely be represented by a closed enum. A future Category Management bounded context will introduce a tenant-owned `Category` entity while retaining enum-backed category lifecycle states. + +## AI and MCP Model + +The application exposes an MCP server. Any MCP-compatible AI client can discover the app’s tools and operate on the user’s task list within the same AIFA contracts, validation, audit trail, and capability boundaries as the web UI. + +The app also supports Ollama as a locally configured model provider for task-plan generation. Cloud model providers may be added behind the same provider-neutral capability. No feature imports a provider SDK directly. + +Examples of MCP operations: + +- create a categorized task; +- list or filter tasks; +- propose a task plan from a natural-language goal; +- move a task to Todo, InProgress, or Completed; +- delete a task when the user explicitly requests it. + +## Trust Principles + +- AI suggestions are reviewable before becoming tasks. +- Status changes are explicit, validated, and audited. +- The same API and feature rules apply whether work originates in the browser, an MCP client, or a local Ollama-assisted workflow. +- Users retain control over categories, priorities, task state, and their selected AI provider. diff --git a/demo/ai-todo-assistant/contexts/ai-planning/contracts/events/v1/task-plan-generated.schema.json b/demo/ai-todo-assistant/contexts/ai-planning/contracts/events/v1/task-plan-generated.schema.json new file mode 100644 index 0000000..6884559 --- /dev/null +++ b/demo/ai-todo-assistant/contexts/ai-planning/contracts/events/v1/task-plan-generated.schema.json @@ -0,0 +1 @@ +{"$schema":"https://json-schema.org/draft/2020-12/schema","title":"TaskPlanGeneratedV1","type":"object","additionalProperties":false,"required":["eventId","eventType","schemaVersion","tenantId","actorId","correlationId","causationId","idempotencyKey","planId","occurredAt"],"properties":{"eventId":{"type":"string"},"eventType":{"const":"TaskPlanGeneratedV1"},"schemaVersion":{"const":1},"tenantId":{"type":"string"},"actorId":{"type":"string"},"correlationId":{"type":"string"},"causationId":{"type":"string"},"idempotencyKey":{"type":"string"},"planId":{"type":"string"},"occurredAt":{"type":"string","format":"date-time"}}} diff --git a/demo/ai-todo-assistant/contexts/ai-planning/contracts/v1/task-plan-suggestion.schema.json b/demo/ai-todo-assistant/contexts/ai-planning/contracts/v1/task-plan-suggestion.schema.json new file mode 100644 index 0000000..8003fb2 --- /dev/null +++ b/demo/ai-todo-assistant/contexts/ai-planning/contracts/v1/task-plan-suggestion.schema.json @@ -0,0 +1,14 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://aifa.dev/ai-planning/contracts/v1/task-plan-suggestion.schema.json", + "title": "Task Plan Suggestion V1", + "type": "object", + "additionalProperties": false, + "required": ["title", "category", "priority", "rationale"], + "properties": { + "title": { "type": "string", "minLength": 1, "maxLength": 240 }, + "category": { "enum": ["Work", "Personal", "Sport", "Shopping", "Other"] }, + "priority": { "enum": ["Low", "Medium", "High"] }, + "rationale": { "type": "string", "maxLength": 1000 } + } +} diff --git a/demo/ai-todo-assistant/contexts/ai-planning/features/generate-task-plan/IMPLEMENTATION_PLAN.md b/demo/ai-todo-assistant/contexts/ai-planning/features/generate-task-plan/IMPLEMENTATION_PLAN.md new file mode 100644 index 0000000..a5c0403 --- /dev/null +++ b/demo/ai-todo-assistant/contexts/ai-planning/features/generate-task-plan/IMPLEMENTATION_PLAN.md @@ -0,0 +1,39 @@ +# Generate Task Plan Feature Plan + +## Goal + +Turn a user goal into a persisted, reviewable task plan. This is the AI-assisted product capability and remains isolated from task persistence. + +## Business Need + +This is the core time-saving promise of the product: a user states an outcome instead of manually decomposing it into tickets. The feature lets local Ollama or any configured provider propose a categorized plan while keeping final acceptance and task creation under user control. + +## Manifest + +- `FeatureName.GenerateTaskPlan` +- `HttpMethod.Post /api/assistant/task-plan` +- frontend contribution: `SlotName.AssistantPanel`, name `generate-task-plan-panel` + +## Backend Contract + +Input: `{ goal: string; category: TaskCategory; priority: TaskPriority }`. + +Success output: `{ suggestions: TaskSuggestion[] }`, where each suggestion has title, `TaskCategory`, enum priority, and rationale. + +Failures: `ApiErrorCode.InvalidInput`, `ApiErrorCode.AssistantUnavailable`, and `ApiErrorCode.AssistantResponseInvalid`. + +Declared capabilities: `CapabilityName.AssistantGenerateTaskPlan` and `CapabilityName.AuditRecord`. + +The feature validates the goal, calls the provider-neutral capability, validates and normalizes its result into enums, persists a tenant-owned `TaskPlan` review record, and records `AuditEventType.TaskPlanGenerated`. It never imports an AI SDK or creates tasks itself. + +## Frontend Contribution + +Render an MUI `TextField`, priority selector, and generation button in `AssistantPanel`. Render suggestions in MUI `Card` components. Each accepted suggestion invokes the create-task route; this keeps creation inside the Create Task feature. + +## Tests + +- rejects a blank goal; +- normalizes generated priority values to `TaskPriority`; +- returns provider failures as `AssistantUnavailable`; +- does not persist tasks directly; +- accepts one suggestion through the Create Task API boundary. diff --git a/demo/ai-todo-assistant/contexts/ai-planning/features/generate-task-plan/contracts/input.schema.json b/demo/ai-todo-assistant/contexts/ai-planning/features/generate-task-plan/contracts/input.schema.json new file mode 100644 index 0000000..998ffb0 --- /dev/null +++ b/demo/ai-todo-assistant/contexts/ai-planning/features/generate-task-plan/contracts/input.schema.json @@ -0,0 +1 @@ +{"$schema":"https://json-schema.org/draft/2020-12/schema","type":"object","additionalProperties":false,"required":["goal","category","priority","idempotencyKey"],"properties":{"goal":{"type":"string","minLength":1,"maxLength":4000},"category":{"enum":["Work","Personal","Sport","Shopping","Other"]},"priority":{"enum":["Low","Medium","High"]},"idempotencyKey":{"type":"string","minLength":16,"maxLength":128}}} diff --git a/demo/ai-todo-assistant/contexts/ai-planning/features/generate-task-plan/contracts/output.schema.json b/demo/ai-todo-assistant/contexts/ai-planning/features/generate-task-plan/contracts/output.schema.json new file mode 100644 index 0000000..6b3385d --- /dev/null +++ b/demo/ai-todo-assistant/contexts/ai-planning/features/generate-task-plan/contracts/output.schema.json @@ -0,0 +1 @@ +{"$schema":"https://json-schema.org/draft/2020-12/schema","type":"object","additionalProperties":false,"required":["suggestions"],"properties":{"suggestions":{"type":"array","items":{"$ref":"https://aifa.dev/ai-planning/contracts/v1/task-plan-suggestion.schema.json"}}}} diff --git a/demo/ai-todo-assistant/contexts/ai-planning/features/generate-task-plan/feature.definition.json b/demo/ai-todo-assistant/contexts/ai-planning/features/generate-task-plan/feature.definition.json new file mode 100644 index 0000000..8ff9f24 --- /dev/null +++ b/demo/ai-todo-assistant/contexts/ai-planning/features/generate-task-plan/feature.definition.json @@ -0,0 +1,18 @@ +{ + "$schema": "../../../../core/architecture/feature-definition.schema.json", + "name": "generate-task-plan", + "boundedContext": "ai-planning", + "businessNeed": "Save users from manually decomposing goals by generating reviewable, categorized task suggestions from natural language.", + "dependsOn": [], + "consumesContracts": ["task-management/contracts/v1/task-taxonomy.schema.json"], + "backend": { "route": "/api/assistant/task-plan", "method": "POST", "capabilities": ["AssistantGenerateTaskPlan", "AuditRecord"], "mcp": { "exposed": true, "toolName": "generate_task_plan" } }, + "frontend": { "slots": ["AssistantPanel"] }, + "contracts": { "input": "contracts/input.schema.json", "output": "contracts/output.schema.json", "mcpInput": "contracts/input.schema.json" }, + "events": ["ai-planning/contracts/events/v1/task-plan-generated.schema.json"], + "security": { "mutation": true, "actorRequired": true, "idempotent": true, "requiresConfirmation": false, "concurrency": "none", "mcpScopes": ["AssistantPlanGenerate"] }, + "workItems": [ + { "id": "AIP-1", "description": "Define a provider-neutral AI planning capability and Ollama adapter.", "acceptanceCriterion": "The feature has no direct dependency on an AI SDK." }, + { "id": "AIP-2", "description": "Validate AI suggestions into TaskCategory and TaskPriority enums.", "acceptanceCriterion": "Invalid provider output returns AssistantResponseInvalid." }, + { "id": "AIP-3", "description": "Register the MUI suggestion-review panel.", "acceptanceCriterion": "Accepted suggestions use the create-task API boundary." } + ] +} diff --git a/demo/ai-todo-assistant/contexts/task-management/contracts/events/v1/task-created.schema.json b/demo/ai-todo-assistant/contexts/task-management/contracts/events/v1/task-created.schema.json new file mode 100644 index 0000000..a9c0725 --- /dev/null +++ b/demo/ai-todo-assistant/contexts/task-management/contracts/events/v1/task-created.schema.json @@ -0,0 +1 @@ +{"$schema":"https://json-schema.org/draft/2020-12/schema","title":"TaskCreatedV1","type":"object","additionalProperties":false,"required":["eventId","eventType","schemaVersion","tenantId","actorId","correlationId","causationId","idempotencyKey","taskId","occurredAt"],"properties":{"eventId":{"type":"string"},"eventType":{"const":"TaskCreatedV1"},"schemaVersion":{"const":1},"tenantId":{"type":"string"},"actorId":{"type":"string"},"correlationId":{"type":"string"},"causationId":{"type":"string"},"idempotencyKey":{"type":"string"},"taskId":{"type":"string"},"occurredAt":{"type":"string","format":"date-time"}}} diff --git a/demo/ai-todo-assistant/contexts/task-management/contracts/events/v1/task-deleted.schema.json b/demo/ai-todo-assistant/contexts/task-management/contracts/events/v1/task-deleted.schema.json new file mode 100644 index 0000000..e03f2c4 --- /dev/null +++ b/demo/ai-todo-assistant/contexts/task-management/contracts/events/v1/task-deleted.schema.json @@ -0,0 +1 @@ +{"$schema":"https://json-schema.org/draft/2020-12/schema","title":"TaskDeletedV1","type":"object","additionalProperties":false,"required":["eventId","eventType","schemaVersion","tenantId","actorId","correlationId","causationId","idempotencyKey","taskId","occurredAt"],"properties":{"eventId":{"type":"string"},"eventType":{"const":"TaskDeletedV1"},"schemaVersion":{"const":1},"tenantId":{"type":"string"},"actorId":{"type":"string"},"correlationId":{"type":"string"},"causationId":{"type":"string"},"idempotencyKey":{"type":"string"},"taskId":{"type":"string"},"occurredAt":{"type":"string","format":"date-time"}}} diff --git a/demo/ai-todo-assistant/contexts/task-management/contracts/events/v1/task-status-changed.schema.json b/demo/ai-todo-assistant/contexts/task-management/contracts/events/v1/task-status-changed.schema.json new file mode 100644 index 0000000..69d6a16 --- /dev/null +++ b/demo/ai-todo-assistant/contexts/task-management/contracts/events/v1/task-status-changed.schema.json @@ -0,0 +1 @@ +{"$schema":"https://json-schema.org/draft/2020-12/schema","title":"TaskStatusChangedV1","type":"object","additionalProperties":false,"required":["eventId","eventType","schemaVersion","tenantId","actorId","correlationId","causationId","idempotencyKey","taskId","previousStatus","status","version","occurredAt"],"properties":{"eventId":{"type":"string"},"eventType":{"const":"TaskStatusChangedV1"},"schemaVersion":{"const":1},"tenantId":{"type":"string"},"actorId":{"type":"string"},"correlationId":{"type":"string"},"causationId":{"type":"string"},"idempotencyKey":{"type":"string"},"taskId":{"type":"string"},"previousStatus":{"enum":["Todo","InProgress","Completed"]},"status":{"enum":["Todo","InProgress","Completed"]},"version":{"type":"integer","minimum":1},"occurredAt":{"type":"string","format":"date-time"}}} diff --git a/demo/ai-todo-assistant/contexts/task-management/contracts/v1/task-taxonomy.schema.json b/demo/ai-todo-assistant/contexts/task-management/contracts/v1/task-taxonomy.schema.json new file mode 100644 index 0000000..4dc01d3 --- /dev/null +++ b/demo/ai-todo-assistant/contexts/task-management/contracts/v1/task-taxonomy.schema.json @@ -0,0 +1,13 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://aifa.dev/task-management/contracts/v1/task-taxonomy.schema.json", + "title": "Task Management Taxonomy V1", + "type": "object", + "additionalProperties": false, + "required": ["category", "priority", "status"], + "properties": { + "category": { "enum": ["Work", "Personal", "Sport", "Shopping", "Other"] }, + "priority": { "enum": ["Low", "Medium", "High"] }, + "status": { "enum": ["Todo", "InProgress", "Completed"] } + } +} diff --git a/demo/ai-todo-assistant/contexts/task-management/contracts/v1/task-view.schema.json b/demo/ai-todo-assistant/contexts/task-management/contracts/v1/task-view.schema.json new file mode 100644 index 0000000..613b5dd --- /dev/null +++ b/demo/ai-todo-assistant/contexts/task-management/contracts/v1/task-view.schema.json @@ -0,0 +1,19 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://aifa.dev/task-management/contracts/v1/task-view.schema.json", + "title": "TaskView V1", + "type": "object", + "additionalProperties": false, + "required": ["id", "title", "category", "priority", "status", "version", "createdAt", "updatedAt", "completedAt"], + "properties": { + "id": { "type": "string", "minLength": 1 }, + "title": { "type": "string", "minLength": 1, "maxLength": 240 }, + "category": { "enum": ["Work", "Personal", "Sport", "Shopping", "Other"] }, + "priority": { "enum": ["Low", "Medium", "High"] }, + "status": { "enum": ["Todo", "InProgress", "Completed"] }, + "version": { "type": "integer", "minimum": 1 }, + "createdAt": { "type": "string", "format": "date-time" }, + "updatedAt": { "type": "string", "format": "date-time" }, + "completedAt": { "type": ["string", "null"], "format": "date-time" } + } +} diff --git a/demo/ai-todo-assistant/contexts/task-management/contracts/v1/task.schema.json b/demo/ai-todo-assistant/contexts/task-management/contracts/v1/task.schema.json new file mode 100644 index 0000000..2cac399 --- /dev/null +++ b/demo/ai-todo-assistant/contexts/task-management/contracts/v1/task.schema.json @@ -0,0 +1,20 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://aifa.dev/task-management/contracts/v1/task.schema.json", + "title": "Task V1", + "type": "object", + "additionalProperties": false, + "required": ["id", "tenantId", "title", "category", "priority", "status", "version", "createdAt", "updatedAt", "completedAt"], + "properties": { + "id": { "type": "string", "minLength": 1 }, + "tenantId": { "type": "string", "minLength": 1 }, + "title": { "type": "string", "minLength": 1, "maxLength": 240 }, + "category": { "enum": ["Work", "Personal", "Sport", "Shopping", "Other"] }, + "priority": { "enum": ["Low", "Medium", "High"] }, + "status": { "enum": ["Todo", "InProgress", "Completed"] }, + "version": { "type": "integer", "minimum": 1 }, + "createdAt": { "type": "string", "format": "date-time" }, + "updatedAt": { "type": "string", "format": "date-time" }, + "completedAt": { "type": ["string", "null"], "format": "date-time" } + } +} diff --git a/demo/ai-todo-assistant/contexts/task-management/features/change-task-status/IMPLEMENTATION_PLAN.md b/demo/ai-todo-assistant/contexts/task-management/features/change-task-status/IMPLEMENTATION_PLAN.md new file mode 100644 index 0000000..62dbe54 --- /dev/null +++ b/demo/ai-todo-assistant/contexts/task-management/features/change-task-status/IMPLEMENTATION_PLAN.md @@ -0,0 +1,43 @@ +# Change Task Status Feature Plan + +## Goal + +Move a task between `TaskStatus.Todo`, `TaskStatus.InProgress`, and `TaskStatus.Completed` through one explicit, auditable business operation. + +## Business Need + +An assistant is only useful if the list stays current. Users need to tell an AI “I started this” or “I finished that” and trust that the resulting ticket state is valid, visible, and recorded. A single status-transition feature gives the web app, MCP clients, and Ollama-assisted workflows the same controlled operation. + +## Manifest + +- `FeatureName.ChangeTaskStatus` +- `HttpMethod.Patch /api/tasks/:taskId/status` +- MCP tool: `change_task_status` +- frontend contribution: `SlotName.TaskRowActions`, name `change-task-status-action` + +## Backend Contract + +Input: `{ taskId: string; status: TaskStatus }`. + +Success output: `{ task: Task }` with the requested enum status. + +Failures: `ApiErrorCode.NotFound` and `ApiErrorCode.InvalidStatusTransition`. + +Declared capabilities: `CapabilityName.TaskLoad`, `CapabilityName.TaskSave`, `CapabilityName.ClockNow`, and `CapabilityName.AuditRecord`. + +The feature validates all transitions before saving. Moving to `Completed` sets `completedAt`; moving from `Completed` clears it. Repeating the current status succeeds without a duplicate audit event. A changed state writes the matching `AuditEventType`. + +The web UI may expose Complete and Reopen controls, but they are presentation conveniences that submit this feature’s `TaskStatus.Completed` or `TaskStatus.Todo` command. MCP clients use the same generic status-transition tool, including for `TaskStatus.InProgress`. + +## Frontend Contribution + +Render an MUI status `Select` or menu on each task row using `TaskStatus` enum values. The contribution calls this feature’s route and refreshes the shared task model. It must not mutate local task state directly. + +## Tests + +- moves a task through every supported `TaskStatus` transition; +- sets and clears `completedAt` correctly; +- accepts idempotent status requests without duplicate audit entries; +- returns `NotFound` for an unknown task; +- rejects invalid external strings before feature execution; +- exposes the same result through the HTTP route and MCP tool. diff --git a/demo/ai-todo-assistant/contexts/task-management/features/change-task-status/contracts/input.schema.json b/demo/ai-todo-assistant/contexts/task-management/features/change-task-status/contracts/input.schema.json new file mode 100644 index 0000000..99f2265 --- /dev/null +++ b/demo/ai-todo-assistant/contexts/task-management/features/change-task-status/contracts/input.schema.json @@ -0,0 +1 @@ +{"$schema":"https://json-schema.org/draft/2020-12/schema","type":"object","additionalProperties":false,"required":["taskId","status","expectedVersion","idempotencyKey"],"properties":{"taskId":{"type":"string","minLength":1},"status":{"enum":["Todo","InProgress","Completed"]},"expectedVersion":{"type":"integer","minimum":1},"idempotencyKey":{"type":"string","minLength":16,"maxLength":128}}} diff --git a/demo/ai-todo-assistant/contexts/task-management/features/change-task-status/contracts/output.schema.json b/demo/ai-todo-assistant/contexts/task-management/features/change-task-status/contracts/output.schema.json new file mode 100644 index 0000000..42eb5c4 --- /dev/null +++ b/demo/ai-todo-assistant/contexts/task-management/features/change-task-status/contracts/output.schema.json @@ -0,0 +1 @@ +{"$schema":"https://json-schema.org/draft/2020-12/schema","type":"object","additionalProperties":false,"required":["task"],"properties":{"task":{"$ref":"https://aifa.dev/task-management/contracts/v1/task-view.schema.json"}}} diff --git a/demo/ai-todo-assistant/contexts/task-management/features/change-task-status/feature.definition.json b/demo/ai-todo-assistant/contexts/task-management/features/change-task-status/feature.definition.json new file mode 100644 index 0000000..fbc5fe4 --- /dev/null +++ b/demo/ai-todo-assistant/contexts/task-management/features/change-task-status/feature.definition.json @@ -0,0 +1,18 @@ +{ + "$schema": "../../../../core/architecture/feature-definition.schema.json", + "name": "change-task-status", + "boundedContext": "task-management", + "businessNeed": "Keep task lists trustworthy by letting people and AI explicitly move work through Todo, InProgress, and Completed states.", + "dependsOn": [], + "consumesContracts": [], + "backend": { "route": "/api/tasks/:taskId/status", "method": "PATCH", "capabilities": ["TaskLoad", "TaskSave", "ClockNow", "AuditRecord"], "mcp": { "exposed": true, "toolName": "change_task_status" } }, + "frontend": { "slots": ["TaskRowActions"] }, + "contracts": { "input": "contracts/input.schema.json", "output": "contracts/output.schema.json", "mcpInput": "contracts/input.schema.json" }, + "events": ["task-management/contracts/events/v1/task-status-changed.schema.json"], + "security": { "mutation": true, "actorRequired": true, "idempotent": true, "requiresConfirmation": false, "concurrency": "optimistic", "mcpScopes": ["TaskWrite"] }, + "workItems": [ + { "id": "STS-1", "description": "Model allowed TaskStatus transitions as a task-management domain invariant.", "acceptanceCriterion": "Invalid transitions return InvalidStatusTransition." }, + { "id": "STS-2", "description": "Implement the status-transition feature and audit event mapping.", "acceptanceCriterion": "Completion timestamps are set and cleared correctly." }, + { "id": "STS-3", "description": "Register MUI and MCP status controls.", "acceptanceCriterion": "HTTP and MCP calls produce the same validated result." } + ] +} diff --git a/demo/ai-todo-assistant/contexts/task-management/features/create-task/IMPLEMENTATION_PLAN.md b/demo/ai-todo-assistant/contexts/task-management/features/create-task/IMPLEMENTATION_PLAN.md new file mode 100644 index 0000000..d5b8ec6 --- /dev/null +++ b/demo/ai-todo-assistant/contexts/task-management/features/create-task/IMPLEMENTATION_PLAN.md @@ -0,0 +1,39 @@ +# Create Task Feature Plan + +## Goal + +Create one Todo task from a user-provided title and priority. + +## Business Need + +People need a fast, trustworthy way to capture an obligation in the correct life area. This feature is the controlled endpoint used by both humans and AI after a suggestion is accepted, ensuring every created task has an explicit category, priority, and audit record. + +## Manifest + +- `FeatureName.CreateTask` +- `HttpMethod.Post /api/tasks` +- frontend contribution: `SlotName.TaskComposer`, name `create-task-form` + +## Backend Contract + +Input: `{ title: string; category: TaskCategory; priority: TaskPriority }`. + +Success output: `{ task: Task }`. + +Failures: `ApiErrorCode.InvalidInput` for a blank title. + +Declared capabilities: `CapabilityName.TaskCreate`, `CapabilityName.IdCreate`, `CapabilityName.ClockNow`, and `CapabilityName.AuditRecord`. + +Implementation trims the title, creates a `Task` with `TaskStatus.Todo`, persists it through `TaskCreate`, then records `AuditEventType.TaskCreated`. + +## Frontend Contribution + +Render an MUI `TextField`, category `Select`, priority `Select`, and `Button` in the `TaskComposer` slot. Options are derived from `TaskCategory` and `TaskPriority`. Submit calls this feature’s route, refreshes the shared task query model, and presents errors through MUI `Alert`. + +## Tests + +- creates a Todo task with each `TaskPriority` enum value; +- trims a valid title; +- rejects a blank title; +- records `TaskCreated`; +- renders the typed slot contribution. diff --git a/demo/ai-todo-assistant/contexts/task-management/features/create-task/contracts/input.schema.json b/demo/ai-todo-assistant/contexts/task-management/features/create-task/contracts/input.schema.json new file mode 100644 index 0000000..22b7780 --- /dev/null +++ b/demo/ai-todo-assistant/contexts/task-management/features/create-task/contracts/input.schema.json @@ -0,0 +1 @@ +{"$schema":"https://json-schema.org/draft/2020-12/schema","type":"object","additionalProperties":false,"required":["title","category","priority","idempotencyKey"],"properties":{"title":{"type":"string","minLength":1,"maxLength":240},"category":{"enum":["Work","Personal","Sport","Shopping","Other"]},"priority":{"enum":["Low","Medium","High"]},"idempotencyKey":{"type":"string","minLength":16,"maxLength":128}}} diff --git a/demo/ai-todo-assistant/contexts/task-management/features/create-task/contracts/output.schema.json b/demo/ai-todo-assistant/contexts/task-management/features/create-task/contracts/output.schema.json new file mode 100644 index 0000000..42eb5c4 --- /dev/null +++ b/demo/ai-todo-assistant/contexts/task-management/features/create-task/contracts/output.schema.json @@ -0,0 +1 @@ +{"$schema":"https://json-schema.org/draft/2020-12/schema","type":"object","additionalProperties":false,"required":["task"],"properties":{"task":{"$ref":"https://aifa.dev/task-management/contracts/v1/task-view.schema.json"}}} diff --git a/demo/ai-todo-assistant/contexts/task-management/features/create-task/feature.definition.json b/demo/ai-todo-assistant/contexts/task-management/features/create-task/feature.definition.json new file mode 100644 index 0000000..22f1ac6 --- /dev/null +++ b/demo/ai-todo-assistant/contexts/task-management/features/create-task/feature.definition.json @@ -0,0 +1,18 @@ +{ + "$schema": "../../../../core/architecture/feature-definition.schema.json", + "name": "create-task", + "boundedContext": "task-management", + "businessNeed": "Capture a categorized commitment quickly and consistently, whether it originates from a person or an accepted AI suggestion.", + "dependsOn": [], + "consumesContracts": [], + "backend": { "route": "/api/tasks", "method": "POST", "capabilities": ["TaskCreate", "IdCreate", "ClockNow", "AuditRecord"], "mcp": { "exposed": true, "toolName": "create_task" } }, + "frontend": { "slots": ["TaskComposer"] }, + "contracts": { "input": "contracts/input.schema.json", "output": "contracts/output.schema.json", "mcpInput": "contracts/input.schema.json" }, + "events": ["task-management/contracts/events/v1/task-created.schema.json"], + "security": { "mutation": true, "actorRequired": true, "idempotent": true, "requiresConfirmation": false, "concurrency": "none", "mcpScopes": ["TaskWrite"] }, + "workItems": [ + { "id": "CRT-1", "description": "Define validated create-task input using TaskCategory and TaskPriority enums.", "acceptanceCriterion": "Invalid category, priority, or blank title returns a typed failure." }, + { "id": "CRT-2", "description": "Implement and test the AIFA feature using only declared capabilities.", "acceptanceCriterion": "A task is persisted as Todo and audited." }, + { "id": "CRT-3", "description": "Register the MUI composer slot contribution.", "acceptanceCriterion": "A submitted form refreshes the typed task model." } + ] +} diff --git a/demo/ai-todo-assistant/contexts/task-management/features/delete-task/IMPLEMENTATION_PLAN.md b/demo/ai-todo-assistant/contexts/task-management/features/delete-task/IMPLEMENTATION_PLAN.md new file mode 100644 index 0000000..7e1c748 --- /dev/null +++ b/demo/ai-todo-assistant/contexts/task-management/features/delete-task/IMPLEMENTATION_PLAN.md @@ -0,0 +1,38 @@ +# Delete Task Feature Plan + +## Goal + +Permanently remove one task when the user confirms the action. + +## Business Need + +Users need to remove incorrect, duplicated, or no-longer-relevant commitments so the assistant remains useful rather than becoming another source of noise. Explicit confirmation protects against accidental or over-eager AI deletion. + +## Manifest + +- `FeatureName.DeleteTask` +- `HttpMethod.Delete /api/tasks/:taskId` +- frontend contribution: `SlotName.TaskRowActions`, name `delete-task-action` + +## Backend Contract + +Input: `{ taskId: string }`. + +Success output: `{ deleted: true }`. + +Failures: `ApiErrorCode.NotFound`. + +Declared capabilities: `CapabilityName.TaskLoad`, `CapabilityName.TaskDelete`, and `CapabilityName.AuditRecord`. + +The feature checks existence before deletion so it can return a meaningful enum-based failure. It records `AuditEventType.TaskDeleted`. + +## Frontend Contribution + +Render an MUI destructive `IconButton`. Confirmation uses MUI `Dialog`; confirmed deletion calls only this feature route and refreshes the task list. + +## Tests + +- deletes an existing task; +- returns `NotFound` without deleting anything for an unknown ID; +- records `TaskDeleted`; +- requires UI confirmation before the API call. diff --git a/demo/ai-todo-assistant/contexts/task-management/features/delete-task/contracts/input.schema.json b/demo/ai-todo-assistant/contexts/task-management/features/delete-task/contracts/input.schema.json new file mode 100644 index 0000000..7a59e0b --- /dev/null +++ b/demo/ai-todo-assistant/contexts/task-management/features/delete-task/contracts/input.schema.json @@ -0,0 +1 @@ +{"$schema":"https://json-schema.org/draft/2020-12/schema","type":"object","additionalProperties":false,"required":["taskId","expectedVersion","idempotencyKey","confirmed"],"properties":{"taskId":{"type":"string","minLength":1},"expectedVersion":{"type":"integer","minimum":1},"idempotencyKey":{"type":"string","minLength":16,"maxLength":128},"confirmed":{"const":true}}} diff --git a/demo/ai-todo-assistant/contexts/task-management/features/delete-task/contracts/output.schema.json b/demo/ai-todo-assistant/contexts/task-management/features/delete-task/contracts/output.schema.json new file mode 100644 index 0000000..c5f6d42 --- /dev/null +++ b/demo/ai-todo-assistant/contexts/task-management/features/delete-task/contracts/output.schema.json @@ -0,0 +1 @@ +{"$schema":"https://json-schema.org/draft/2020-12/schema","type":"object","additionalProperties":false,"required":["deleted"],"properties":{"deleted":{"const":true}}} diff --git a/demo/ai-todo-assistant/contexts/task-management/features/delete-task/feature.definition.json b/demo/ai-todo-assistant/contexts/task-management/features/delete-task/feature.definition.json new file mode 100644 index 0000000..5a2961e --- /dev/null +++ b/demo/ai-todo-assistant/contexts/task-management/features/delete-task/feature.definition.json @@ -0,0 +1,17 @@ +{ + "$schema": "../../../../core/architecture/feature-definition.schema.json", + "name": "delete-task", + "boundedContext": "task-management", + "businessNeed": "Keep the task list useful by removing incorrect or obsolete commitments with explicit user confirmation.", + "dependsOn": [], + "consumesContracts": [], + "backend": { "route": "/api/tasks/:taskId", "method": "DELETE", "capabilities": ["TaskLoad", "TaskDelete", "AuditRecord"], "mcp": { "exposed": true, "toolName": "delete_task" } }, + "frontend": { "slots": ["TaskRowActions"] }, + "contracts": { "input": "contracts/input.schema.json", "output": "contracts/output.schema.json", "mcpInput": "contracts/input.schema.json" }, + "events": ["task-management/contracts/events/v1/task-deleted.schema.json"], + "security": { "mutation": true, "actorRequired": true, "idempotent": true, "requiresConfirmation": true, "concurrency": "optimistic", "mcpScopes": ["TaskDelete"] }, + "workItems": [ + { "id": "DEL-1", "description": "Implement existence-aware deletion and audit behavior.", "acceptanceCriterion": "Unknown IDs return NotFound and do not produce an audit record." }, + { "id": "DEL-2", "description": "Register the confirmed MUI deletion action.", "acceptanceCriterion": "The HTTP request occurs only after dialog confirmation." } + ] +} diff --git a/demo/ai-todo-assistant/contexts/task-management/features/list-tasks/IMPLEMENTATION_PLAN.md b/demo/ai-todo-assistant/contexts/task-management/features/list-tasks/IMPLEMENTATION_PLAN.md new file mode 100644 index 0000000..0f7594b --- /dev/null +++ b/demo/ai-todo-assistant/contexts/task-management/features/list-tasks/IMPLEMENTATION_PLAN.md @@ -0,0 +1,38 @@ +# List Tasks Feature Plan + +## Goal + +Provide the task collection for the board and its summary slots. + +## Business Need + +Users need one reliable view of commitments across work, personal, sport, shopping, and other categories. This feature gives the web app and connected AI clients the same current, filterable source of truth. + +## Manifest + +- `FeatureName.ListTasks` +- `HttpMethod.Get /api/tasks` +- frontend contribution: `SlotName.TaskList`, name `task-list` +- frontend contribution: `SlotName.TaskSummary`, name `task-summary` + +## Backend Contract + +Input: `{ status?: TaskStatus; category?: TaskCategory }`. + +Success output: `{ tasks: Task[] }` in newest-first creation order. + +Declared capabilities: `CapabilityName.TaskList`. + +The feature optionally filters by `TaskStatus` and `TaskCategory`; the MongoDB capability owns query construction and index usage. + +## Frontend Contribution + +Render the board using MUI `List`, `ListItem`, `Chip`, and `Typography`. The summary contribution uses MUI `Card` components to show active, completed, and priority counts. It consumes the typed task-list slot model only. + +## Tests + +- lists deterministic task order; +- filters by every `TaskStatus` enum value; +- returns an empty collection successfully; +- renders no task rows for an empty model; +- computes summary counts from typed tasks. diff --git a/demo/ai-todo-assistant/contexts/task-management/features/list-tasks/contracts/input.schema.json b/demo/ai-todo-assistant/contexts/task-management/features/list-tasks/contracts/input.schema.json new file mode 100644 index 0000000..84e4f78 --- /dev/null +++ b/demo/ai-todo-assistant/contexts/task-management/features/list-tasks/contracts/input.schema.json @@ -0,0 +1 @@ +{"$schema":"https://json-schema.org/draft/2020-12/schema","type":"object","additionalProperties":false,"properties":{"category":{"enum":["Work","Personal","Sport","Shopping","Other"]},"status":{"enum":["Todo","InProgress","Completed"]}}} diff --git a/demo/ai-todo-assistant/contexts/task-management/features/list-tasks/contracts/output.schema.json b/demo/ai-todo-assistant/contexts/task-management/features/list-tasks/contracts/output.schema.json new file mode 100644 index 0000000..ebeaf14 --- /dev/null +++ b/demo/ai-todo-assistant/contexts/task-management/features/list-tasks/contracts/output.schema.json @@ -0,0 +1 @@ +{"$schema":"https://json-schema.org/draft/2020-12/schema","type":"object","additionalProperties":false,"required":["tasks"],"properties":{"tasks":{"type":"array","items":{"$ref":"https://aifa.dev/task-management/contracts/v1/task-view.schema.json"}}}} diff --git a/demo/ai-todo-assistant/contexts/task-management/features/list-tasks/feature.definition.json b/demo/ai-todo-assistant/contexts/task-management/features/list-tasks/feature.definition.json new file mode 100644 index 0000000..6184ba5 --- /dev/null +++ b/demo/ai-todo-assistant/contexts/task-management/features/list-tasks/feature.definition.json @@ -0,0 +1,18 @@ +{ + "$schema": "../../../../core/architecture/feature-definition.schema.json", + "name": "list-tasks", + "boundedContext": "task-management", + "businessNeed": "Give people and connected AI clients a single filterable source of truth for commitments across life categories.", + "dependsOn": [], + "consumesContracts": [], + "backend": { "route": "/api/tasks", "method": "GET", "capabilities": ["TaskList"], "mcp": { "exposed": true, "toolName": "list_tasks" } }, + "frontend": { "slots": ["TaskList", "TaskSummary"] }, + "contracts": { "input": "contracts/input.schema.json", "output": "contracts/output.schema.json", "mcpInput": "contracts/input.schema.json" }, + "events": [], + "security": { "mutation": false, "actorRequired": true, "idempotent": true, "requiresConfirmation": false, "concurrency": "none", "mcpScopes": ["TaskRead"] }, + "workItems": [ + { "id": "LST-1", "description": "Define category and status filtering with enum validation.", "acceptanceCriterion": "Filters accept only TaskCategory and TaskStatus values." }, + { "id": "LST-2", "description": "Implement the read-only AIFA feature and Mongo capability query.", "acceptanceCriterion": "Results are deterministically ordered." }, + { "id": "LST-3", "description": "Register MUI list and summary slot contributions.", "acceptanceCriterion": "The empty, filtered, and populated states render correctly." } + ] +} diff --git a/demo/ai-todo-assistant/core/IMPLEMENTATION_PLAN.md b/demo/ai-todo-assistant/core/IMPLEMENTATION_PLAN.md new file mode 100644 index 0000000..d8dbe8e --- /dev/null +++ b/demo/ai-todo-assistant/core/IMPLEMENTATION_PLAN.md @@ -0,0 +1,129 @@ +# Core Implementation Plan + +## Responsibility + +`core/` provides composition and infrastructure. It does not contain task or AI-assistant business behavior. + +## Folder Layout + +```txt +core/ + shared/ + enums.ts + task.ts + feature.ts + feature-manifest.ts + mcp-tool.ts + backend/ + runtime/ + capabilities/ + database/ + discovery/ + http/ + mcp/ + server.ts + frontend/ + app-shell/ + slots/ + api/ + theme/ + discovery/ + architecture/ +contexts/ + task-management/ + domain/ + features/ + ai-planning/ + domain/ + features/ +``` + +## Shared Contracts + +`shared/enums.ts` defines closed internal sets with TypeScript enums: + +- `FeatureName` +- `CapabilityName` +- `SlotName` +- `TaskPriority` +- `TaskCategory` +- `TaskStatus` +- `ApiErrorCode` +- `AuditEventType` +- `HttpMethod` + +`shared/task.ts` defines the MongoDB-persisted `Task` model. It uses `TaskCategory`, `TaskPriority`, and `TaskStatus`, never raw category, status, or priority strings. Initial category enum values are Work, Personal, Sport, Shopping, and Other; initial status enum values are Todo, InProgress, and Completed. + +`shared/feature.ts` defines `AifaFeature`, `FeatureContext`, `FeatureResult`, and explicit failure results. A feature context contains only input, actor, metadata, declared capabilities, `ok`, and `fail`. + +`shared/feature-manifest.ts` defines a manifest that joins a feature-local backend registration and frontend registration under one `FeatureName`. + +## Dynamic Registration + +Every feature contains `feature.definition.json`, validated against `core/architecture/feature-definition.schema.json`, and exports `manifest.ts` from its own folder. The JSON definition is the machine-readable technical work contract; the TypeScript manifest binds it to executable backend and frontend registrations. + +A manifest exposes: + +```ts +export const manifest: FeatureManifest = { + name: FeatureName.CreateTask, + backend: { route, method, feature }, + frontend: { contributions }, +}; +``` + +### Backend discovery + +`core/backend/discovery/discoverFeatures.ts` reads `contexts/*/features/*/feature.definition.json`, validates the schema and dependency DAG, imports the corresponding `manifest.ts`, and returns a deterministic list ordered by `FeatureName`. + +Validation fails at startup for duplicate feature names, duplicate method-and-route pairs, duplicate slot contribution names, unknown enum values, or a capability not declared by a feature. + +Feature manifests may also expose an MCP tool definition. `core/backend/mcp/` dynamically discovers those definitions and registers MCP tools that invoke the exact same AIFA feature. MCP clients therefore cannot bypass business validation or audit records. + +The HTTP router receives discovered route registrations and invokes the declared AIFA feature. It contains no task-specific switch statements. + +### Frontend discovery + +`core/frontend/discovery/discoverFeatures.ts` uses Vite `import.meta.glob` to load every `contexts/*/features/*/manifest.ts`. It registers each frontend contribution with the typed slot registry in deterministic order. + +The React app shell knows only the slot contract; it never imports a product feature directly. + +## Backend Infrastructure + +### MongoDB + +`core/backend/database/mongo.ts` owns one MongoDB client connection using `MONGODB_URI` and exposes typed collection access. Feature code never imports this module. + +`core/backend/capabilities/taskCapabilities.ts` adapts MongoDB operations to runtime capabilities such as `TaskCreate`, `TaskLoad`, `TaskSave`, and `TaskList`. + +`core/backend/capabilities/assistantCapabilities.ts` will expose the AI provider through an `AssistantGenerateTaskPlan` capability. The provider is configured in core; the AI feature receives only the capability. + +`core/backend/capabilities/ollamaAssistantProvider.ts` is the initial provider adapter. It calls a locally configured Ollama instance and maps its response into typed task suggestions. Cloud providers can be added through the same interface without changing the AI feature. + +### Runtime + +The runtime grants a feature only its manifest-declared capability enums. Attempts to access an undeclared capability return `ApiErrorCode.CapabilityNotAllowed`. Runtime-owned auditing records `AuditEventType` values in MongoDB. + +## Frontend Infrastructure + +The app shell uses MUI only: `ThemeProvider`, `CssBaseline`, `Container`, `Stack`, `Paper`, `AppBar`, and standard MUI controls. Do not create custom CSS components. + +`core/frontend/slots/Slot.tsx` renders contributions registered for a `SlotName`. Every slot has a TypeScript model interface keyed by `SlotName`; contributions are type-checked against that model. + +The initial app-shell slots are: + +- `TaskComposer` +- `TaskList` +- `TaskRowActions` +- `TaskSummary` +- `AssistantPanel` + +## Cross-Cutting Tests + +- Manifest discovery rejects duplicates and produces a stable order. +- The runtime denies undeclared capabilities. +- Route registration invokes a discovered feature rather than hard-coded product logic. +- Slot registration validates models and ordered contributions. +- MongoDB integration tests run against a disposable test database. +- Feature-definition JSON validates against the schema and dependency graph. +- dependency-cruiser enforces `core/`, bounded-context, feature, and adapter import boundaries. diff --git a/demo/ai-todo-assistant/core/architecture/.dependency-cruiser.cjs b/demo/ai-todo-assistant/core/architecture/.dependency-cruiser.cjs new file mode 100644 index 0000000..d9770b0 --- /dev/null +++ b/demo/ai-todo-assistant/core/architecture/.dependency-cruiser.cjs @@ -0,0 +1,12 @@ +module.exports = { + forbidden: [ + { name: "no-circular-imports", severity: "error", from: {}, to: { circular: true } }, + { name: "core-cannot-import-contexts", severity: "error", from: { path: "^core/" }, to: { path: "^contexts/" } }, + { name: "task-management-cannot-import-ai-planning-implementation", severity: "error", from: { path: "^contexts/task-management/(domain|features)/" }, to: { path: "^contexts/ai-planning/(domain|features)/" } }, + { name: "ai-planning-cannot-import-task-management-implementation", severity: "error", from: { path: "^contexts/ai-planning/(domain|features)/" }, to: { path: "^contexts/task-management/(domain|features)/" } } + ], + options: { + doNotFollow: { path: "node_modules" }, + tsConfig: { fileName: "tsconfig.json" } + } +}; diff --git a/demo/ai-todo-assistant/core/architecture/dependency-policy.json b/demo/ai-todo-assistant/core/architecture/dependency-policy.json new file mode 100644 index 0000000..ec44350 --- /dev/null +++ b/demo/ai-todo-assistant/core/architecture/dependency-policy.json @@ -0,0 +1,16 @@ +{ + "layers": ["core", "bounded-context-domain", "feature", "adapter"], + "allowedImports": { + "core": ["core"], + "bounded-context-domain": ["core", "bounded-context-domain"], + "feature": ["core", "bounded-context-domain", "feature-self"], + "adapter": ["core", "feature"] + }, + "forbidden": [ + "core-to-context-or-feature", + "feature-to-other-feature", + "cross-context-domain-import", + "circular-import" + ], + "crossContextRule": "Use versioned contracts or domain events; never import another context's domain or feature module." +} diff --git a/demo/ai-todo-assistant/core/architecture/feature-definition.schema.json b/demo/ai-todo-assistant/core/architecture/feature-definition.schema.json new file mode 100644 index 0000000..934d480 --- /dev/null +++ b/demo/ai-todo-assistant/core/architecture/feature-definition.schema.json @@ -0,0 +1,82 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://aifa.dev/schemas/feature-definition.schema.json", + "title": "AIFA Feature Definition", + "type": "object", + "additionalProperties": false, + "required": ["name", "boundedContext", "businessNeed", "dependsOn", "consumesContracts", "backend", "frontend", "contracts", "events", "security", "workItems"], + "properties": { + "$schema": { "type": "string" }, + "name": { "type": "string", "pattern": "^[a-z]+(?:-[a-z]+)*$" }, + "boundedContext": { "type": "string", "pattern": "^[a-z]+(?:-[a-z]+)*$" }, + "businessNeed": { "type": "string", "minLength": 20 }, + "dependsOn": { "type": "array", "uniqueItems": true, "items": { "type": "string", "pattern": "^[a-z]+(?:-[a-z]+)*$" } }, + "consumesContracts": { "type": "array", "uniqueItems": true, "items": { "type": "string", "pattern": "^[a-z-]+/contracts/v[0-9]+/.+\\.schema\\.json$" } }, + "backend": { + "type": "object", + "additionalProperties": false, + "required": ["route", "method", "capabilities", "mcp"], + "properties": { + "route": { "type": "string", "pattern": "^/api/" }, + "method": { "enum": ["GET", "POST", "PATCH", "DELETE"] }, + "capabilities": { "type": "array", "uniqueItems": true, "items": { "type": "string" } }, + "mcp": { + "type": "object", + "additionalProperties": false, + "required": ["exposed"], + "properties": { "exposed": { "type": "boolean" }, "toolName": { "type": "string" } } + } + } + }, + "frontend": { + "type": "object", + "additionalProperties": false, + "required": ["slots"], + "properties": { "slots": { "type": "array", "items": { "type": "string" } } } + }, + "contracts": { + "type": "object", + "additionalProperties": false, + "required": ["input", "output"], + "properties": { + "input": { "type": "string", "pattern": "^contracts/.+\\.schema\\.json$" }, + "output": { "type": "string", "pattern": "^contracts/.+\\.schema\\.json$" }, + "mcpInput": { "type": "string", "pattern": "^contracts/.+\\.schema\\.json$" } + } + }, + "security": { + "type": "object", + "additionalProperties": false, + "required": ["mutation", "actorRequired", "idempotent", "requiresConfirmation", "mcpScopes", "concurrency"], + "properties": { + "mutation": { "type": "boolean" }, + "actorRequired": { "type": "boolean" }, + "idempotent": { "type": "boolean" }, + "requiresConfirmation": { "type": "boolean" }, + "concurrency": { "enum": ["none", "optimistic"] }, + "mcpScopes": { "type": "array", "uniqueItems": true, "items": { "type": "string" } } + } + }, + "events": { "type": "array", "uniqueItems": true, "items": { "type": "string", "pattern": "^[a-z-]+/contracts/events/v[0-9]+/.+\\.schema\\.json$" } }, + "workItems": { + "type": "array", + "minItems": 1, + "items": { + "type": "object", + "additionalProperties": false, + "required": ["id", "description", "acceptanceCriterion"], + "properties": { + "id": { "type": "string", "pattern": "^[A-Z]+-[0-9]+$" }, + "description": { "type": "string" }, + "acceptanceCriterion": { "type": "string" } + } + } + } + }, + "allOf": [ + { + "if": { "type": "object", "properties": { "backend": { "type": "object", "properties": { "mcp": { "type": "object", "properties": { "exposed": { "const": true } }, "required": ["exposed"] } }, "required": ["mcp"] } }, "required": ["backend"] }, + "then": { "type": "object", "properties": { "backend": { "type": "object", "properties": { "mcp": { "type": "object", "properties": { "toolName": { "type": "string" } }, "required": ["toolName"] } } } } } + } + ] +} diff --git a/demo/ai-todo-assistant/core/architecture/generate-contract-types.ts b/demo/ai-todo-assistant/core/architecture/generate-contract-types.ts new file mode 100644 index 0000000..47b279f --- /dev/null +++ b/demo/ai-todo-assistant/core/architecture/generate-contract-types.ts @@ -0,0 +1,43 @@ +import { mkdir, readdir, readFile, writeFile } from "node:fs/promises"; +import { dirname, join, relative, resolve } from "node:path"; +import { fileURLToPath } from "node:url"; + +import { compile } from "json-schema-to-typescript"; + +const appRoot = resolve(dirname(fileURLToPath(import.meta.url)), "../.."); +const contextsRoot = join(appRoot, "contexts"); +const outputFile = join(appRoot, "core/shared/generated/contracts.ts"); + +async function findSchemaFiles(directory: string): Promise { + const entries = await readdir(directory, { withFileTypes: true }); + const files = await Promise.all(entries.map(async (entry) => { + const location = join(directory, entry.name); + if (entry.isDirectory()) return findSchemaFiles(location); + return location.endsWith(".schema.json") ? [location] : []; + })); + return files.flat(); +} + +async function main(): Promise { + const files = (await findSchemaFiles(contextsRoot)).sort(); + const schemas = await Promise.all(files.map(async (file) => ({ file, schema: JSON.parse(await readFile(file, "utf8")) as Record }))); + const schemasById = new Map(schemas.flatMap(({ schema }) => typeof schema.$id === "string" ? [[schema.$id, schema] as const] : [])); + const expandReferences = (value: unknown, seen = new Set()): unknown => { + if (Array.isArray(value)) return value.map((item) => expandReferences(item, seen)); + if (!value || typeof value !== "object") return value; + const object = value as Record; + if (typeof object.$ref === "string" && Object.keys(object).length === 1 && schemasById.has(object.$ref)) { + if (seen.has(object.$ref)) return object; + return expandReferences(schemasById.get(object.$ref), new Set([...seen, object.$ref])); + } + return Object.fromEntries(Object.entries(object).map(([key, item]) => [key, expandReferences(item, seen)])); + }; + const declarations = await Promise.all(schemas.map(({ file, schema }) => + compile(expandReferences(schema) as object, relative(contextsRoot, file).replaceAll(/[/.\-]/g, "_"), { bannerComment: "" }), + )); + await mkdir(dirname(outputFile), { recursive: true }); + await writeFile(outputFile, `/* Generated by npm run generate:contracts. Do not edit manually. */\n\n${declarations.join("\n")}`); + console.log(`Generated ${files.length} contract type declarations.`); +} + +void main(); diff --git a/demo/ai-todo-assistant/core/architecture/validate-feature-definitions.ts b/demo/ai-todo-assistant/core/architecture/validate-feature-definitions.ts new file mode 100644 index 0000000..c75bb03 --- /dev/null +++ b/demo/ai-todo-assistant/core/architecture/validate-feature-definitions.ts @@ -0,0 +1,112 @@ +import { existsSync } from "node:fs"; +import { readdir, readFile } from "node:fs/promises"; +import { dirname, join, relative, resolve } from "node:path"; +import { fileURLToPath, pathToFileURL } from "node:url"; +import { createRequire } from "node:module"; + +import { CapabilityName, FeatureName, HttpMethod, PermissionScope, SlotName } from "../shared/architecture-enums.js"; + +interface FeatureDefinition { + name: string; + dependsOn: string[]; + consumesContracts: string[]; + events: string[]; + backend: { route: string; method: string; capabilities: string[]; mcp: { exposed: boolean; toolName?: string } }; + frontend: { slots: string[] }; + contracts: { input: string; output: string; mcpInput?: string }; + security: { mutation: boolean; actorRequired: boolean; idempotent: boolean; requiresConfirmation: boolean; mcpScopes: string[]; concurrency: "none" | "optimistic" }; +} + +const appRoot = resolve(dirname(fileURLToPath(import.meta.url)), "../.."); +const contextsRoot = join(appRoot, "contexts"); +const require = createRequire(import.meta.url); +const Ajv2020 = require("ajv/dist/2020").default; +const addFormats = require("ajv-formats").default; + +async function findFiles(directory: string, predicate: (file: string) => boolean): Promise { + const entries = await readdir(directory, { withFileTypes: true }); + const files = await Promise.all(entries.map(async (entry) => { + const location = join(directory, entry.name); + if (entry.isDirectory()) return findFiles(location, predicate); + return predicate(location) ? [location] : []; + })); + return files.flat(); +} + +async function readJson(file: string): Promise { + return JSON.parse(await readFile(file, "utf8")) as T; +} + +function assertEnumValue(value: string, values: object, label: string): void { + if (!Object.values(values).includes(value)) throw new Error(`${label} has unknown enum value '${value}'`); +} + +function assertExistingFile(base: string, reference: string, label: string): string { + const file = resolve(base, reference); + if (!existsSync(file)) throw new Error(`${label} references missing file '${reference}'`); + return file; +} + +function assertAcyclicFeatureDependencies(features: FeatureDefinition[]): void { + const byName = new Map(features.map((feature) => [feature.name, feature])); + const visiting = new Set(); + const visited = new Set(); + const visit = (name: string, path: string[]): void => { + if (visiting.has(name)) throw new Error(`Circular feature dependency: ${[...path, name].join(" -> ")}`); + if (visited.has(name)) return; + const feature = byName.get(name); + if (!feature) throw new Error(`Feature '${path.at(-1)}' depends on unknown feature '${name}'`); + visiting.add(name); + for (const dependency of feature.dependsOn) visit(dependency, [...path, name]); + visiting.delete(name); + visited.add(name); + }; + for (const feature of features) visit(feature.name, []); +} + +async function main(): Promise { + const definitionSchema = await readJson(join(appRoot, "core/architecture/feature-definition.schema.json")); + const schemaFiles = await findFiles(contextsRoot, (file) => file.endsWith(".schema.json")); + const ajv = new Ajv2020({ allErrors: true, strict: true }); + addFormats(ajv); + for (const schemaFile of schemaFiles) ajv.addSchema(await readJson(schemaFile), pathToFileURL(schemaFile).href); + const validateDefinition = ajv.compile(definitionSchema); + for (const schemaFile of schemaFiles) ajv.getSchema(pathToFileURL(schemaFile).href) ?? ajv.compile(await readJson(schemaFile)); + + const definitionFiles = await findFiles(contextsRoot, (file) => file.endsWith("feature.definition.json")); + const definitions = await Promise.all(definitionFiles.map(async (file) => ({ file, definition: await readJson(file) }))); + const routes = new Set(); + const tools = new Set(); + + for (const { file, definition } of definitions) { + if (!validateDefinition(definition)) throw new Error(`${relative(appRoot, file)} fails schema validation: ${ajv.errorsText(validateDefinition.errors)}`); + assertEnumValue(definition.name, FeatureName, definition.name); + assertEnumValue(definition.backend.method, HttpMethod, definition.name); + for (const capability of definition.backend.capabilities) assertEnumValue(capability, CapabilityName, definition.name); + for (const slot of definition.frontend.slots) assertEnumValue(slot, SlotName, definition.name); + for (const scope of definition.security.mcpScopes) assertEnumValue(scope, PermissionScope, definition.name); + if (!definition.security.actorRequired || !definition.security.idempotent) throw new Error(`${definition.name} must require an actor and be idempotent`); + if (definition.backend.mcp.exposed && definition.security.mcpScopes.length === 0) throw new Error(`${definition.name} exposes MCP without a scope`); + if (definition.backend.method === HttpMethod.Delete && !definition.security.requiresConfirmation) throw new Error(`${definition.name} deletion requires confirmation`); + const routeKey = `${definition.backend.method} ${definition.backend.route}`; + if (routes.has(routeKey)) throw new Error(`Duplicate route ${routeKey}`); + routes.add(routeKey); + if (definition.backend.mcp.exposed) { + if (!definition.backend.mcp.toolName) throw new Error(`${definition.name} exposes MCP without a tool name`); + if (tools.has(definition.backend.mcp.toolName)) throw new Error(`Duplicate MCP tool ${definition.backend.mcp.toolName}`); + tools.add(definition.backend.mcp.toolName); + } + + const featureDirectory = dirname(file); + const inputSchema = await readJson<{ required?: string[] }>(assertExistingFile(featureDirectory, definition.contracts.input, definition.name)); + assertExistingFile(featureDirectory, definition.contracts.output, definition.name); + if (definition.contracts.mcpInput) assertExistingFile(featureDirectory, definition.contracts.mcpInput, definition.name); + if (definition.security.mutation && !inputSchema.required?.includes("idempotencyKey")) throw new Error(`${definition.name} mutation input requires idempotencyKey`); + if (definition.security.concurrency === "optimistic" && !inputSchema.required?.includes("expectedVersion")) throw new Error(`${definition.name} optimistic mutation input requires expectedVersion`); + for (const contract of [...definition.consumesContracts, ...definition.events]) assertExistingFile(contextsRoot, contract, definition.name); + } + assertAcyclicFeatureDependencies(definitions.map(({ definition }) => definition)); + console.log(`Validated ${definitions.length} feature definitions, schemas, contracts, routes, MCP tools, enums, and dependency graph.`); +} + +void main(); diff --git a/demo/ai-todo-assistant/core/shared/architecture-enums.ts b/demo/ai-todo-assistant/core/shared/architecture-enums.ts new file mode 100644 index 0000000..90ebf4a --- /dev/null +++ b/demo/ai-todo-assistant/core/shared/architecture-enums.ts @@ -0,0 +1,41 @@ +export enum FeatureName { + CreateTask = "create-task", + ListTasks = "list-tasks", + ChangeTaskStatus = "change-task-status", + DeleteTask = "delete-task", + GenerateTaskPlan = "generate-task-plan", +} + +export enum CapabilityName { + TaskCreate = "TaskCreate", + TaskList = "TaskList", + TaskLoad = "TaskLoad", + TaskSave = "TaskSave", + TaskDelete = "TaskDelete", + IdCreate = "IdCreate", + ClockNow = "ClockNow", + AuditRecord = "AuditRecord", + AssistantGenerateTaskPlan = "AssistantGenerateTaskPlan", +} + +export enum SlotName { + TaskComposer = "TaskComposer", + TaskList = "TaskList", + TaskSummary = "TaskSummary", + TaskRowActions = "TaskRowActions", + AssistantPanel = "AssistantPanel", +} + +export enum PermissionScope { + TaskRead = "TaskRead", + TaskWrite = "TaskWrite", + TaskDelete = "TaskDelete", + AssistantPlanGenerate = "AssistantPlanGenerate", +} + +export enum HttpMethod { + Get = "GET", + Post = "POST", + Patch = "PATCH", + Delete = "DELETE", +} diff --git a/demo/ai-todo-assistant/package-lock.json b/demo/ai-todo-assistant/package-lock.json new file mode 100644 index 0000000..dc12852 --- /dev/null +++ b/demo/ai-todo-assistant/package-lock.json @@ -0,0 +1,1419 @@ +{ + "name": "ai-todo-assistant", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "ai-todo-assistant", + "version": "0.1.0", + "devDependencies": { + "@types/node": "^22.10.2", + "ajv": "^8.17.1", + "ajv-formats": "^3.0.1", + "dependency-cruiser": "^16.10.0", + "json-schema-to-typescript": "^15.0.4", + "tsx": "^4.19.2", + "typescript": "^5.7.2" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/@apidevtools/json-schema-ref-parser": { + "version": "11.9.3", + "resolved": "https://registry.npmjs.org/@apidevtools/json-schema-ref-parser/-/json-schema-ref-parser-11.9.3.tgz", + "integrity": "sha512-60vepv88RwcJtSHrD6MjIL6Ta3SOYbgfnkHb+ppAVK+o9mXprRtulx7VlRl3lN3bbvysAfCS7WMVfhUYemB0IQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jsdevtools/ono": "^7.1.3", + "@types/json-schema": "^7.0.15", + "js-yaml": "^4.1.0" + }, + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://github.com/sponsors/philsturgeon" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.2.tgz", + "integrity": "sha512-XExcO+dvLKvVtNTibSTBej1NCAbaGhWn9Ww1ZPx80qsahhPFe/8jgWP0IchNe0F3HwkU7n8ejhH8bjonqht8mQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.2.tgz", + "integrity": "sha512-kXXoiPVVGQcnIYGOeaovwOURpniDBpSq4A03qkQ+BMQqtGG6HYap3xne9C1O1yo4TR3qxlCX5IqqmX6fFo2Lqg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.2.tgz", + "integrity": "sha512-5YfKeeI8qWfBZIX+u2xZC3Zlb3Os/gLS2sbEKM+I4ZOcsWmHS2WLysCcQZDAFRslDUU5Oiq44gf6PYN1vGwG5A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.2.tgz", + "integrity": "sha512-O387ite7SzUyCcy3JQX4P4bLtEA7bLLkx+esve5JHnyYfNTxcVpXZo9jhdB0lTKN44gztELTdU7nS8Nr16Fs1Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.2.tgz", + "integrity": "sha512-n4KqkOQrraxHJcgjM1RvwbigfQKIKJVpM7xp+KsxiyUSrRdIXnt73VhrPAx0fV44hgfmIVKjxMN9J1t5jySVkw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.2.tgz", + "integrity": "sha512-uq6suIWYP37qzGddBKPw5QEQPi6HiLGsO7UmkpfyaYNQ3D+rN6w6WfwH+nuqcGXWvawGwxOEroO4YGnFh95azw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.2.tgz", + "integrity": "sha512-n+I0BTSRIoy+d6RPKnEVwql5UwBJolytvY4mAOIEJorKlqgPII8ix6slVVrfZ5Tnj7glIZvloylbB/EJPMWEXw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.2.tgz", + "integrity": "sha512-78XJTJkvPs0kz2w61301PJjXl4g7q3JqiYMZ/M/yVI73EHBrCRTgkhu9oqG7vPqq+a/yadEW8aD+agKlk5xrmg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.2.tgz", + "integrity": "sha512-XlDnu2q5yoqems+xay6wSAcg9DDD7K9RLKZEBOMZm3ckNpJBvOX20tSfby8KfrrhINDyv9V2YVZKY/SpoGJI8w==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.2.tgz", + "integrity": "sha512-pW4AC0P3it8c7do9MVM4p51FzHzdM/TZrerurgRcHJ2WTa1VQ1CIq18xncfpBJw4ojkiZZrKW2yIBWBP92j6Ug==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.2.tgz", + "integrity": "sha512-CYbnj78HsIeA+DhgUKgFCfvNsTHFhMMrinUrMZpDXJXKN8T3XViTZ/+wtHeVxEWY8ewSzTFN+nRmSwO2tZaLUQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.2.tgz", + "integrity": "sha512-buwkd8nsph4R+ajRvw0qM5Hja/TXQow3ptzWO2EbG/cqcIkHloRrdlBtQlshyYGTNFvfkfJ5tpPLVkY4DtsPfQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.2.tgz", + "integrity": "sha512-ZVykbDyk7519VwiNb9Lcj9m8XM6v5V9uKPvrEMkkEedVewf+0itkhahp4HDpgERXhwLRpWFypsGbG/J8s0QjJA==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.2.tgz", + "integrity": "sha512-CAXl+Dtd9UUuJd8pKKdwh6MLm3MUMiqMPmhZ3tTSXPqfyQ3vDl6R5hZdZ/kYojK4ofXtdfSv1tFq8XzWx3heNQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.2.tgz", + "integrity": "sha512-GeXCej4IQtU1B+QlDV8W/RRvbzI3O/Stss+/bCXv4lZls5WGRtu2a+3JkA3i4qIUlMXpcHebWpF8AkJhATowuA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.2.tgz", + "integrity": "sha512-3H1weTYZPxt/WOhByszQZybS9w5lKzUn1FDMsgEChbHWQwHYQQRfBxgCcZvPhjHfKyJjIievvMmEUawJrdY9Dg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.2.tgz", + "integrity": "sha512-4xTZr1FUmSoQW4XIWmit3tzQrUTZM+N3P0XV8xROKYF50XfI7xeO90+1bZvNwxIufQ9hDQVRJH5YhgPVF8A/HQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.2.tgz", + "integrity": "sha512-sSATRjPeDBg3pdgHoQfoYBob11Kk1FGa9lui5RIHZCoCkJa9QKlvl3/vKz2usCmYYjs7ymJR/2Nnsqe+Hjt5nw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.2.tgz", + "integrity": "sha512-lqnzCV+mM0gIADaKihiCg6ifgfU2L3h5E33rNQBN1Y4MaVGnzryzmvvf7UHxprpQdE8hpqLolJ9Rl+SkIRDpyw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.2.tgz", + "integrity": "sha512-AL2qJILH7lNjrDmCQDvdxMfAUIv8KMNZOvrwAQ8i8//ntL9FflhOyMJ8OZSMBb8/AWXe3/5v5S20y3zCoZWKoQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.2.tgz", + "integrity": "sha512-QtiuPytchRyC4rwUKhexJdQKvDuZ6hWloi3igqPQNUJCS1/v9EiO3UTOXR6A3FoMo4fnAKbWJdqaIwhOzh8qEw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.2.tgz", + "integrity": "sha512-WkhYDmpTjLvGlScA1rwjRUmhl4k8oXR3cIbtqWmELgU/dFeHHlEllxDvdWcNJV9rbzCexB5vz8gtNewWLgCT7Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.2.tgz", + "integrity": "sha512-GPMSkTOtMnv2U2F8gxe4Io6qmVs+YKyp832Etqqxr0hFngmXQ3rzwytelm3GIn7T4VviRUlf3sOgBOiTdvaf7g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.2.tgz", + "integrity": "sha512-PIhhEkE9uPBleRBrQEJpUn7MBnibZzbGzYWPmY3x+YoVg/95zbjB4CxPPOQ8l5tYYM4mMaCthF8/1DIfBQQyWQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.2.tgz", + "integrity": "sha512-YmJbfTlvU7Sdn9BB+4PRES4oB6pxgS37MAONj+hBr/cpXS1aBPKXxNnDbu+QCWPj0o9dgyxeq79g6c5P8KeuYA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.2.tgz", + "integrity": "sha512-5ebpxr3nWMzrL/rnUI755Jkuee0bHL/Gq0WTF9lvcpv73wAp5eu8MfBUgWK9bhWvZjj7yX8etf/8tI8Ney695g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@jsdevtools/ono": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/@jsdevtools/ono/-/ono-7.1.3.tgz", + "integrity": "sha512-4JQNk+3mVzK3xh2rqd6RB4J46qUR19azEHBneZyTZM+c456qOrbbM/5xcR8huNCCcbVt7+UmizG6GuUvPvKUYg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/lodash": { + "version": "4.17.25", + "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.25.tgz", + "integrity": "sha512-+K1NIO8I+F9/wNulfVvu23QYd0Pe9/OCqRrim4NoYIf1VoEDL90Ve4ClzpyqBLc7NpGGWRvYNCKZ1BE/Jpf8dQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "22.20.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.20.1.tgz", + "integrity": "sha512-EANqOCF9QFyra+4pfxUcX9STKJpCLjMbObVzljIJomAWSnuSIEAvyzEU53GaajbXJEgdh0iEcPL+DGvpUd4k1Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/acorn": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.18.0.tgz", + "integrity": "sha512-lGq+9yr1/GuAWaVYIHRjvvySG5/4VfKIvC8EWxStPdcDh/Ka7FG3twP6v4d5BkravUilhIAsG4Qj83t02LWUPQ==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/acorn-jsx-walk": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/acorn-jsx-walk/-/acorn-jsx-walk-2.0.0.tgz", + "integrity": "sha512-uuo6iJj4D4ygkdzd6jPtcxs8vZgDX9YFIkqczGImoypX2fQ4dVImmu3UzA4ynixCIMTrEOWW+95M2HuBaCEOVA==", + "dev": true, + "license": "MIT" + }, + "node_modules/acorn-loose": { + "version": "8.5.2", + "resolved": "https://registry.npmjs.org/acorn-loose/-/acorn-loose-8.5.2.tgz", + "integrity": "sha512-PPvV6g8UGMGgjrMu+n/f9E/tCSkNQ2Y97eFvuVdJfG11+xdIeDcLyNdC8SHcrHbRqkfwLASdplyR6B6sKM1U4A==", + "dev": true, + "license": "MIT", + "dependencies": { + "acorn": "^8.15.0" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-walk": { + "version": "8.3.5", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.5.tgz", + "integrity": "sha512-HEHNfbars9v4pgpW6SO1KSPkfoS0xVOM/9UzkJltjlsHZmJasxg8aXkuZa7SMf8vKGIBhpUsPluQSqhJFCqebw==", + "dev": true, + "license": "MIT", + "dependencies": { + "acorn": "^8.11.0" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/ajv": { + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz", + "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ajv-formats": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-3.0.1.tgz", + "integrity": "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true, + "license": "Python-2.0" + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/commander": { + "version": "13.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-13.1.0.tgz", + "integrity": "sha512-/rFeCpNJQbhSZjGVwO9RFV3xPqbnERS8MmIQzCtD/zl6gpJuV/bMLuN92oG3F7d8oDEHHRrujSXNUr8fpjntKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/dependency-cruiser": { + "version": "16.10.4", + "resolved": "https://registry.npmjs.org/dependency-cruiser/-/dependency-cruiser-16.10.4.tgz", + "integrity": "sha512-hrxVOjIm8idZ9ZVDGSyyG3SHiNcEUPhL6RTEmO/3wfQWLepH5pA3nuDMMrcJ1DkZztFA7xg3tk8OVO+MmwwH9w==", + "dev": true, + "license": "MIT", + "dependencies": { + "acorn": "^8.15.0", + "acorn-jsx": "^5.3.2", + "acorn-jsx-walk": "^2.0.0", + "acorn-loose": "^8.5.2", + "acorn-walk": "^8.3.4", + "ajv": "^8.17.1", + "commander": "^13.1.0", + "enhanced-resolve": "^5.18.2", + "ignore": "^7.0.5", + "interpret": "^3.1.1", + "is-installed-globally": "^1.0.0", + "json5": "^2.2.3", + "memoize": "^10.1.0", + "picocolors": "^1.1.1", + "picomatch": "^4.0.2", + "prompts": "^2.4.2", + "rechoir": "^0.8.0", + "safe-regex": "^2.1.1", + "semver": "^7.7.2", + "teamcity-service-messages": "^0.1.14", + "tsconfig-paths-webpack-plugin": "^4.2.0", + "watskeburt": "^4.2.3" + }, + "bin": { + "depcruise": "bin/dependency-cruise.mjs", + "depcruise-baseline": "bin/depcruise-baseline.mjs", + "depcruise-fmt": "bin/depcruise-fmt.mjs", + "depcruise-wrap-stream-in-html": "bin/wrap-stream-in-html.mjs", + "dependency-cruise": "bin/dependency-cruise.mjs", + "dependency-cruiser": "bin/dependency-cruise.mjs" + }, + "engines": { + "node": "^18.17||>=20" + } + }, + "node_modules/enhanced-resolve": { + "version": "5.24.5", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.24.5.tgz", + "integrity": "sha512-L1l8TNvomm6UVW5B253AGxQagSQr+vGwhMlrrfRS2qmhx46AMpMVJKQYLvWYbysTMY8VoicOvzHzoHMbyzB+4A==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.4", + "tapable": "^2.3.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/esbuild": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.2.tgz", + "integrity": "sha512-HKVLS8dvII+xoKW9kmqxbRKrnWEXfJJr/FZhhJmiqIB0e053QNYFqOBouTMO/k5sID4MvCiUCvv8b9M4h32wIA==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.28.2", + "@esbuild/android-arm": "0.28.2", + "@esbuild/android-arm64": "0.28.2", + "@esbuild/android-x64": "0.28.2", + "@esbuild/darwin-arm64": "0.28.2", + "@esbuild/darwin-x64": "0.28.2", + "@esbuild/freebsd-arm64": "0.28.2", + "@esbuild/freebsd-x64": "0.28.2", + "@esbuild/linux-arm": "0.28.2", + "@esbuild/linux-arm64": "0.28.2", + "@esbuild/linux-ia32": "0.28.2", + "@esbuild/linux-loong64": "0.28.2", + "@esbuild/linux-mips64el": "0.28.2", + "@esbuild/linux-ppc64": "0.28.2", + "@esbuild/linux-riscv64": "0.28.2", + "@esbuild/linux-s390x": "0.28.2", + "@esbuild/linux-x64": "0.28.2", + "@esbuild/netbsd-arm64": "0.28.2", + "@esbuild/netbsd-x64": "0.28.2", + "@esbuild/openbsd-arm64": "0.28.2", + "@esbuild/openbsd-x64": "0.28.2", + "@esbuild/openharmony-arm64": "0.28.2", + "@esbuild/sunos-x64": "0.28.2", + "@esbuild/win32-arm64": "0.28.2", + "@esbuild/win32-ia32": "0.28.2", + "@esbuild/win32-x64": "0.28.2" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-uri": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.5.tgz", + "integrity": "sha512-gHwA1O9LDIcKunMKhObS/HimwtehO1nPUECKAu5TpKgaO19fcWEl4bliWe1jWxVFvIXztJjjQ4L8XQ1EU9f7Jw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/global-directory": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/global-directory/-/global-directory-4.0.1.tgz", + "integrity": "sha512-wHTUcDUoZ1H5/0iVqEudYW4/kAlN5cZ3j/bXn0Dpbizl9iaUVeWSHqiOjsgk6OW2bkLclbBjzewBz6weQ1zA2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ini": "4.1.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/hasown": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz", + "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==", + "dev": true, + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/ignore": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.6.tgz", + "integrity": "sha512-BAg6QkE8W+TuQLrrw0Ugr7HegXduRuuj8/ti2kSOc+jz1dmx8/WNcjr6XGnq5YpDWxFwwaavqD0+jIUOKelTsw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/ini": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/ini/-/ini-4.1.1.tgz", + "integrity": "sha512-QQnnxNyfvmHFIsj7gkPcYymR8Jdw/o7mp5ZFihxn6h8Ci6fh3Dx4E1gPjpQEpIuPo9XVNY/ZUwh4BPMjGyL01g==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/interpret": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/interpret/-/interpret-3.1.1.tgz", + "integrity": "sha512-6xwYfHbajpoF0xLW+iwLkhwgvLoZDfjYfoFNu8ftMoXINzwuymNLd9u/KmwtdT2GbR+/Cz66otEGEVVUHX9QLQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/is-core-module": { + "version": "2.16.2", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.2.tgz", + "integrity": "sha512-evOr8xfXKxE6qSR0hSXL2r3sd7ALj8+7jQEUvPYcm5sgZFdJ+AYzT6yNmJenvIYQBgIGwfwz08sL8zoL7yq2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "hasown": "^2.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-installed-globally": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-installed-globally/-/is-installed-globally-1.0.0.tgz", + "integrity": "sha512-K55T22lfpQ63N4KEN57jZUAaAYqYHEe8veb/TycJRk9DdSCLLcovXz/mL6mOnhQaZsQGwPhuFopdQIlqGSEjiQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "global-directory": "^4.0.1", + "is-path-inside": "^4.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-path-inside": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-4.0.0.tgz", + "integrity": "sha512-lJJV/5dYS+RcL8uQdBDW9c9uWFLLBNRyFhnAKXw5tVqLlKZ4RMGZKv+YQ/IA3OhD+RpbJa1LLFM1FQPGyIXvOA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/js-yaml": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.1.tgz", + "integrity": "sha512-CY6crGq313MX8GkwvB7tzgp99vjQxY1++5y10/BKN/GUfHqWaOGQMNZkBvqSzsZKWk/ijwHlWzzkLulsGHhjWQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/puzrin" + }, + { + "type": "github", + "url": "https://github.com/sponsors/nodeca" + } + ], + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/json-schema-to-typescript": { + "version": "15.0.4", + "resolved": "https://registry.npmjs.org/json-schema-to-typescript/-/json-schema-to-typescript-15.0.4.tgz", + "integrity": "sha512-Su9oK8DR4xCmDsLlyvadkXzX6+GGXJpbhwoLtOGArAG61dvbW4YQmSEno2y66ahpIdmLMg6YUf/QHLgiwvkrHQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@apidevtools/json-schema-ref-parser": "^11.5.5", + "@types/json-schema": "^7.0.15", + "@types/lodash": "^4.17.7", + "is-glob": "^4.0.3", + "js-yaml": "^4.1.0", + "lodash": "^4.17.21", + "minimist": "^1.2.8", + "prettier": "^3.2.5", + "tinyglobby": "^0.2.9" + }, + "bin": { + "json2ts": "dist/src/cli.js" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true, + "license": "MIT" + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/kleur": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz", + "integrity": "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/lodash": { + "version": "4.18.1", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz", + "integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/memoize": { + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/memoize/-/memoize-10.2.0.tgz", + "integrity": "sha512-DeC6b7QBrZsRs3Y02A6A7lQyzFbsQbqgjI6UW0GigGWV+u1s25TycMr0XHZE4cJce7rY/vyw2ctMQqfDkIhUEA==", + "dev": true, + "license": "MIT", + "dependencies": { + "mimic-function": "^5.0.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sindresorhus/memoize?sponsor=1" + } + }, + "node_modules/mimic-function": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/mimic-function/-/mimic-function-5.0.1.tgz", + "integrity": "sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true, + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.5.tgz", + "integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/prettier": { + "version": "3.9.6", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.9.6.tgz", + "integrity": "sha512-OpN0zzVdiaiAhxpuuj5efpIS4sY9j7bY6uR5mnj5yPzGkdkjNKSJeUThPb60Jw29QuAZgA4o+/iB49kFiaBX6g==", + "dev": true, + "license": "MIT", + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, + "node_modules/prompts": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz", + "integrity": "sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "kleur": "^3.0.3", + "sisteransi": "^1.0.5" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/rechoir": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/rechoir/-/rechoir-0.8.0.tgz", + "integrity": "sha512-/vxpCXddiX8NGfGO/mTafwjq4aFa/71pvamip0++IQk3zG8cbCj0fifNPrjjF1XMXUne91jL9OoxmdykoEtifQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "resolve": "^1.20.0" + }, + "engines": { + "node": ">= 10.13.0" + } + }, + "node_modules/regexp-tree": { + "version": "0.1.27", + "resolved": "https://registry.npmjs.org/regexp-tree/-/regexp-tree-0.1.27.tgz", + "integrity": "sha512-iETxpjK6YoRWJG5o6hXLwvjYAoW+FEZn9os0PD/b6AP6xQwsa/Y7lCVgIixBbUPMfhu+i2LtdeAqVTgGlQarfA==", + "dev": true, + "license": "MIT", + "bin": { + "regexp-tree": "bin/regexp-tree" + } + }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/resolve": { + "version": "1.22.12", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.12.tgz", + "integrity": "sha512-TyeJ1zif53BPfHootBGwPRYT1RUt6oGWsaQr8UyZW/eAm9bKoijtvruSDEmZHm92CwS9nj7/fWttqPCgzep8CA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "is-core-module": "^2.16.1", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/safe-regex": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/safe-regex/-/safe-regex-2.1.1.tgz", + "integrity": "sha512-rx+x8AMzKb5Q5lQ95Zoi6ZbJqwCLkqi3XuJXp5P3rT8OEc6sZCJG5AE5dU3lsgRr/F4Bs31jSlVN+j5KrsGu9A==", + "dev": true, + "license": "MIT", + "dependencies": { + "regexp-tree": "~0.1.1" + } + }, + "node_modules/semver": { + "version": "7.8.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/sisteransi": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", + "integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==", + "dev": true, + "license": "MIT" + }, + "node_modules/strip-bom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", + "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/tapable": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.3.tgz", + "integrity": "sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/teamcity-service-messages": { + "version": "0.1.14", + "resolved": "https://registry.npmjs.org/teamcity-service-messages/-/teamcity-service-messages-0.1.14.tgz", + "integrity": "sha512-29aQwaHqm8RMX74u2o/h1KbMLP89FjNiMxD9wbF2BbWOnbM+q+d1sCEC+MqCc4QW3NJykn77OMpTFw/xTHIc0w==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyglobby": { + "version": "0.2.17", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", + "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tsconfig-paths": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-4.2.0.tgz", + "integrity": "sha512-NoZ4roiN7LnbKn9QqE1amc9DJfzvZXxF4xDavcOWt1BPkdx+m+0gJuPM+S0vCe7zTJMYUP0R8pO2XMr+Y8oLIg==", + "dev": true, + "license": "MIT", + "dependencies": { + "json5": "^2.2.2", + "minimist": "^1.2.6", + "strip-bom": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/tsconfig-paths-webpack-plugin": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/tsconfig-paths-webpack-plugin/-/tsconfig-paths-webpack-plugin-4.2.0.tgz", + "integrity": "sha512-zbem3rfRS8BgeNK50Zz5SIQgXzLafiHjOwUAvk/38/o1jHn/V5QAgVUcz884or7WYcPaH3N2CIfUc2u0ul7UcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.1.0", + "enhanced-resolve": "^5.7.0", + "tapable": "^2.2.1", + "tsconfig-paths": "^4.1.2" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/tsx": { + "version": "4.23.12", + "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.23.12.tgz", + "integrity": "sha512-FDf4L4sYzKtzWYhU/Xm0AQFdTjdIxNo9ElTf2mxXM6k8YMHXzYUe4yODVaXP4V9uMFbVg8c0qyBccK2OOxb45Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "~0.28.0" + }, + "bin": { + "tsx": "dist/cli.mjs" + }, + "engines": { + "node": ">=18.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/watskeburt": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/watskeburt/-/watskeburt-4.2.3.tgz", + "integrity": "sha512-uG9qtQYoHqAsnT711nG5iZc/8M5inSmkGCOp7pFaytKG2aTfIca7p//CjiVzAE4P7hzaYuCozMjNNaLgmhbK5g==", + "dev": true, + "license": "MIT", + "bin": { + "watskeburt": "dist/run-cli.js" + }, + "engines": { + "node": "^18||>=20" + } + } + } +} diff --git a/demo/ai-todo-assistant/package.json b/demo/ai-todo-assistant/package.json new file mode 100644 index 0000000..80587c0 --- /dev/null +++ b/demo/ai-todo-assistant/package.json @@ -0,0 +1,22 @@ +{ + "name": "ai-todo-assistant", + "version": "0.1.0", + "private": true, + "type": "module", + "scripts": { + "validate:architecture": "tsx core/architecture/validate-feature-definitions.ts", + "generate:contracts": "tsx core/architecture/generate-contract-types.ts", + "check:imports": "depcruise --config core/architecture/.dependency-cruiser.cjs .", + "check": "npm run validate:architecture && npm run generate:contracts && npm run check:imports && tsc --noEmit" + }, + "devDependencies": { + "@types/node": "^22.10.2", + "ajv": "^8.17.1", + "ajv-formats": "^3.0.1", + "dependency-cruiser": "^16.10.0", + "json-schema-to-typescript": "^15.0.4", + "tsx": "^4.19.2", + "typescript": "^5.7.2" + }, + "engines": { "node": ">=20" } +} diff --git a/demo/ai-todo-assistant/tsconfig.json b/demo/ai-todo-assistant/tsconfig.json new file mode 100644 index 0000000..6019fd1 --- /dev/null +++ b/demo/ai-todo-assistant/tsconfig.json @@ -0,0 +1,13 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "NodeNext", + "moduleResolution": "NodeNext", + "strict": true, + "noEmit": true, + "skipLibCheck": true, + "esModuleInterop": true, + "allowSyntheticDefaultImports": true + }, + "include": ["core/**/*.ts", "contexts/**/*.ts"] +} From 348e69a3ea4a3360a81316d946770b22bced30a1 Mon Sep 17 00:00:00 2001 From: Adrian Besleaga Date: Sat, 15 Aug 2026 14:10:14 +0200 Subject: [PATCH 3/6] Demo app added --- .github/workflows/ai-todo-assistant.yml | 27 + .gitignore | 4 +- .prettierrc.json | 3 + .vscode/extensions.json | 3 + .vscode/settings.json | 7 + demo/ai-todo-assistant/.env.example | 17 + demo/ai-todo-assistant/AGENTS.md | 2 + .../AIFA-Architecture-Diagrams.md | 378 ++ .../ARCHITECTURE-REMEDIATION-TODO.md | 158 + demo/ai-todo-assistant/ARCHITECTURE.md | 97 +- .../Architecture-Improvements.md | 582 ++ demo/ai-todo-assistant/IMPROVEMENTS.md | 39 + demo/ai-todo-assistant/OPERATIONS.md | 24 + demo/ai-todo-assistant/README.md | 114 + demo/ai-todo-assistant/TODO.md | 72 + .../architecture-presentation.html | 1233 ++++ .../composition/generated/frontend-modules.ts | 39 + .../events/v1/task-plan-generated.schema.json | 31 +- .../contexts/ai-planning/domain/task-plan.ts | 17 + .../ai-planning/domain/task-planning.ts | 34 + .../generate-task-plan/backend/feature.ts | 70 + .../contracts/input.schema.json | 13 +- .../contracts/output.schema.json | 27 +- .../feature.definition.json | 51 +- .../frontend/contribution.tsx | 217 + .../frontend/use-generate-task-plan.ts | 45 + .../features/generate-task-plan/manifest.ts | 20 + .../ai-planning/infrastructure/manifest.ts | 28 + .../mongo-task-plan-capabilities.ts | 22 + .../infrastructure/ollama-task-planner.ts | 146 + .../events/v1/task-created.schema.json | 31 +- .../events/v1/task-deleted.schema.json | 31 +- .../events/v1/task-status-changed.schema.json | 37 +- .../contracts/v1/task-cache.schema.json | 6 + .../contracts/v1/task-cache.ts | 7 + .../contracts/v1/task-events.ts | 51 + .../contracts/v1/task-taxonomy.ts | 18 + .../contracts/v1/task-view.schema.json | 12 +- .../task-management/contracts/v1/task-view.ts | 8 + .../contracts/v1/task.schema.json | 13 +- .../domain/task-capabilities.ts | 22 + .../contexts/task-management/domain/task.ts | 46 + .../change-task-status/backend/feature.ts | 72 + .../contracts/input.schema.json | 13 +- .../contracts/output.schema.json | 11 +- .../feature.definition.json | 56 +- .../frontend/contribution.tsx | 78 + .../frontend/use-change-task-status.ts | 25 + .../features/change-task-status/manifest.ts | 28 + .../features/create-task/backend/feature.ts | 51 + .../create-task/contracts/input.schema.json | 13 +- .../create-task/contracts/output.schema.json | 11 +- .../create-task/feature.definition.json | 56 +- .../create-task/frontend/contribution.tsx | 101 + .../create-task/frontend/use-create-task.ts | 26 + .../features/create-task/manifest.ts | 28 + .../features/delete-task/backend/feature.ts | 44 + .../delete-task/contracts/input.schema.json | 13 +- .../delete-task/contracts/output.schema.json | 9 +- .../delete-task/feature.definition.json | 50 +- .../delete-task/frontend/contribution.tsx | 82 + .../delete-task/frontend/use-delete-task.ts | 19 + .../features/delete-task/manifest.ts | 28 + .../features/list-tasks/backend/feature.ts | 45 + .../list-tasks/contracts/input.schema.json | 13 +- .../list-tasks/contracts/output.schema.json | 15 +- .../list-tasks/feature.definition.json | 44 +- .../list-tasks/frontend/contribution.tsx | 1295 ++++ .../list-tasks/frontend/controller.tsx | 39 + .../features/list-tasks/frontend/use-tasks.ts | 38 + .../features/list-tasks/manifest.ts | 26 + .../infrastructure/manifest.ts | 10 + .../infrastructure/mongo-task-capabilities.ts | 93 + .../manage-ai-settings/IMPLEMENTATION_PLAN.md | 4 + .../manage-ai-settings/backend/feature.ts | 18 + .../contracts/input.schema.json | 6 + .../contracts/output.schema.json | 8 + .../feature.definition.json | 37 + .../frontend/contribution.tsx | 134 + .../features/manage-ai-settings/manifest.ts | 24 + .../core/architecture/.dependency-cruiser.cjs | 70 +- .../core/architecture/check-bundle-size.mjs | 14 + .../core/architecture/copy-server-assets.mjs | 21 + .../feature-definition.schema.json | 102 +- .../architecture/generate-contract-types.ts | 129 +- .../validate-feature-definitions.ts | 148 +- .../core/backend/auth/actor-resolver.ts | 50 + demo/ai-todo-assistant/core/backend/config.ts | 57 + .../core/backend/database/mongo.ts | 41 + .../backend/database/transaction-runner.ts | 3 + .../discovery/context-infrastructure.ts | 43 + .../backend/discovery/discover-features.ts | 181 + .../backend/http/authorization-middleware.ts | 38 + .../core/backend/http/request-handler.ts | 170 + .../core/backend/http/request-parser.ts | 23 + .../core/backend/http/route-matcher.ts | 28 + .../idempotency/canonical-fingerprint.ts | 18 + .../backend/idempotency/mongo-idempotency.ts | 139 + .../core/backend/mcp-http.ts | 72 + .../core/backend/mcp-server.ts | 88 + demo/ai-todo-assistant/core/backend/mcp.ts | 29 + .../core/backend/outbox/mongo-outbox.ts | 112 + .../core/backend/outbox/outbox-publisher.ts | 38 + .../core/backend/outbox/worker.ts | 17 + .../core/backend/runtime/aifa-runtime.ts | 72 + .../runtime/mongo-platform-capabilities.ts | 66 + demo/ai-todo-assistant/core/backend/server.ts | 217 + .../core/backend/telemetry/logger.ts | 16 + demo/ai-todo-assistant/core/frontend/App.tsx | 152 + .../core/frontend/FrontendRuntimeProvider.tsx | 12 + demo/ai-todo-assistant/core/frontend/api.ts | 30 + .../core/frontend/app-model.ts | 32 + .../core/frontend/auth/oidc.ts | 109 + .../discovery/discover-contributions.ts | 54 + .../frontend/events/FrontendEventProvider.tsx | 22 + .../core/frontend/events/event-registry.ts | 59 + demo/ai-todo-assistant/core/frontend/main.tsx | 16 + .../core/frontend/query/query-client.ts | 169 + .../core/frontend/query/react-query.tsx | 95 + .../core/frontend/slots/LazySlot.tsx | 24 + .../core/frontend/slots/Slot.tsx | 14 + .../core/frontend/slots/slot-registry.ts | 57 + demo/ai-todo-assistant/core/frontend/theme.ts | 57 + .../core/frontend/vite-env.d.ts | 5 + demo/ai-todo-assistant/core/shared/aifa.ts | 60 + .../core/shared/architecture-enums.ts | 61 +- .../core/shared/feature-manifest.ts | 52 + .../diagrams/aifa-backend-extension.png | Bin 0 -> 98831 bytes .../diagrams/aifa-backend-extension.svg | 75 + .../diagrams/aifa-frontend-ui-slots.png | Bin 0 -> 99835 bytes .../diagrams/aifa-frontend-ui-slots.svg | 92 + .../diagrams/aifa-system-overview.png | Bin 0 -> 110459 bytes .../diagrams/aifa-system-overview.svg | 109 + demo/ai-todo-assistant/docker-compose.yml | 30 + demo/ai-todo-assistant/index.html | 16 + demo/ai-todo-assistant/package-lock.json | 5210 ++++++++++++++--- demo/ai-todo-assistant/package.json | 38 +- demo/ai-todo-assistant/public/favicon.svg | 1 + .../public/readme-preview.jpg | Bin 0 -> 71347 bytes .../public/readme-workspace.jpg | Bin 0 -> 71462 bytes .../tests/ai-provider.test.ts | 79 + .../tests/built-server.smoke.mjs | 44 + .../tests/core-quality.test.ts | 171 + demo/ai-todo-assistant/tests/features.test.ts | 213 + .../tests/frontend-accessibility.test.tsx | 42 + .../tests/frontend-runtime.test.ts | 95 + .../tests/http-boundary.test.ts | 93 + .../tests/mongo.integration.test.ts | 238 + demo/ai-todo-assistant/tsconfig.json | 5 +- demo/ai-todo-assistant/tsconfig.server.json | 11 + demo/ai-todo-assistant/vite.config.ts | 11 + docs/index.html | 50 +- docs/styles.css | 21 +- examples/nodejs-react-typescript/README.md | 24 +- .../backend/features/completeTask.ts | 25 +- .../backend/features/createTask.ts | 30 +- .../backend/features/deleteTask.ts | 19 +- .../backend/features/listTasks.ts | 10 +- .../backend/features/reopenTask.ts | 19 +- .../backend/runtime/aifaRuntime.ts | 53 +- .../backend/runtime/feature.ts | 16 +- .../nodejs-react-typescript/backend/server.ts | 95 +- .../backend/storage/fileTaskStore.ts | 34 +- .../backend/storage/memoryTaskStore.ts | 24 +- .../examples/tickets/create-task.md | 1 - .../examples/tickets/ui-slots.md | 1 - .../frontend/src/api.ts | 9 +- .../contributions/registerContributions.tsx | 22 +- .../frontend/src/main.tsx | 16 +- .../frontend/src/slots/Slot.tsx | 7 +- .../frontend/src/slots/slotRegistry.ts | 17 +- .../frontend/src/styles.css | 8 +- .../test/slotRegistry.test.js | 5 +- .../test/taskFeatures.test.js | 12 +- examples/tickets/archive-project.md | 1 - examples/vanilla-js/README.md | 13 +- .../backend/features/completeTask.js | 1 - .../vanilla-js/backend/features/createTask.js | 1 - .../vanilla-js/backend/features/deleteTask.js | 1 - .../vanilla-js/backend/features/listTasks.js | 1 - .../vanilla-js/backend/features/reopenTask.js | 1 - .../vanilla-js/backend/runtime/aifaRuntime.js | 15 +- .../vanilla-js/backend/runtime/feature.js | 1 - .../backend/storage/fileTaskStore.js | 1 - .../backend/storage/memoryTaskStore.js | 5 +- .../examples/tickets/complete-task.md | 1 - .../examples/tickets/create-task.md | 1 - .../examples/tickets/delete-task.md | 1 - .../examples/tickets/reopen-task.md | 1 - examples/vanilla-js/frontend/app.js | 11 +- examples/vanilla-js/frontend/index.html | 9 +- examples/vanilla-js/frontend/styles.css | 11 +- examples/vanilla-js/package.json | 1 - examples/vanilla-js/test/taskFeatures.test.js | 1 - package-lock.json | 34 + package.json | 4 +- src/demo.js | 1 - src/features/archiveProject.js | 1 - src/runtime/feature.js | 1 - src/runtime/memoryRuntime.js | 13 +- test/archiveProject.test.js | 1 - 201 files changed, 15825 insertions(+), 946 deletions(-) create mode 100644 .github/workflows/ai-todo-assistant.yml create mode 100644 .prettierrc.json create mode 100644 .vscode/extensions.json create mode 100644 .vscode/settings.json create mode 100644 demo/ai-todo-assistant/.env.example create mode 100644 demo/ai-todo-assistant/AIFA-Architecture-Diagrams.md create mode 100644 demo/ai-todo-assistant/ARCHITECTURE-REMEDIATION-TODO.md create mode 100644 demo/ai-todo-assistant/Architecture-Improvements.md create mode 100644 demo/ai-todo-assistant/IMPROVEMENTS.md create mode 100644 demo/ai-todo-assistant/OPERATIONS.md create mode 100644 demo/ai-todo-assistant/README.md create mode 100644 demo/ai-todo-assistant/TODO.md create mode 100644 demo/ai-todo-assistant/architecture-presentation.html create mode 100644 demo/ai-todo-assistant/composition/generated/frontend-modules.ts create mode 100644 demo/ai-todo-assistant/contexts/ai-planning/domain/task-plan.ts create mode 100644 demo/ai-todo-assistant/contexts/ai-planning/domain/task-planning.ts create mode 100644 demo/ai-todo-assistant/contexts/ai-planning/features/generate-task-plan/backend/feature.ts create mode 100644 demo/ai-todo-assistant/contexts/ai-planning/features/generate-task-plan/frontend/contribution.tsx create mode 100644 demo/ai-todo-assistant/contexts/ai-planning/features/generate-task-plan/frontend/use-generate-task-plan.ts create mode 100644 demo/ai-todo-assistant/contexts/ai-planning/features/generate-task-plan/manifest.ts create mode 100644 demo/ai-todo-assistant/contexts/ai-planning/infrastructure/manifest.ts create mode 100644 demo/ai-todo-assistant/contexts/ai-planning/infrastructure/mongo-task-plan-capabilities.ts create mode 100644 demo/ai-todo-assistant/contexts/ai-planning/infrastructure/ollama-task-planner.ts create mode 100644 demo/ai-todo-assistant/contexts/task-management/contracts/v1/task-cache.schema.json create mode 100644 demo/ai-todo-assistant/contexts/task-management/contracts/v1/task-cache.ts create mode 100644 demo/ai-todo-assistant/contexts/task-management/contracts/v1/task-events.ts create mode 100644 demo/ai-todo-assistant/contexts/task-management/contracts/v1/task-taxonomy.ts create mode 100644 demo/ai-todo-assistant/contexts/task-management/contracts/v1/task-view.ts create mode 100644 demo/ai-todo-assistant/contexts/task-management/domain/task-capabilities.ts create mode 100644 demo/ai-todo-assistant/contexts/task-management/domain/task.ts create mode 100644 demo/ai-todo-assistant/contexts/task-management/features/change-task-status/backend/feature.ts create mode 100644 demo/ai-todo-assistant/contexts/task-management/features/change-task-status/frontend/contribution.tsx create mode 100644 demo/ai-todo-assistant/contexts/task-management/features/change-task-status/frontend/use-change-task-status.ts create mode 100644 demo/ai-todo-assistant/contexts/task-management/features/change-task-status/manifest.ts create mode 100644 demo/ai-todo-assistant/contexts/task-management/features/create-task/backend/feature.ts create mode 100644 demo/ai-todo-assistant/contexts/task-management/features/create-task/frontend/contribution.tsx create mode 100644 demo/ai-todo-assistant/contexts/task-management/features/create-task/frontend/use-create-task.ts create mode 100644 demo/ai-todo-assistant/contexts/task-management/features/create-task/manifest.ts create mode 100644 demo/ai-todo-assistant/contexts/task-management/features/delete-task/backend/feature.ts create mode 100644 demo/ai-todo-assistant/contexts/task-management/features/delete-task/frontend/contribution.tsx create mode 100644 demo/ai-todo-assistant/contexts/task-management/features/delete-task/frontend/use-delete-task.ts create mode 100644 demo/ai-todo-assistant/contexts/task-management/features/delete-task/manifest.ts create mode 100644 demo/ai-todo-assistant/contexts/task-management/features/list-tasks/backend/feature.ts create mode 100644 demo/ai-todo-assistant/contexts/task-management/features/list-tasks/frontend/contribution.tsx create mode 100644 demo/ai-todo-assistant/contexts/task-management/features/list-tasks/frontend/controller.tsx create mode 100644 demo/ai-todo-assistant/contexts/task-management/features/list-tasks/frontend/use-tasks.ts create mode 100644 demo/ai-todo-assistant/contexts/task-management/features/list-tasks/manifest.ts create mode 100644 demo/ai-todo-assistant/contexts/task-management/infrastructure/manifest.ts create mode 100644 demo/ai-todo-assistant/contexts/task-management/infrastructure/mongo-task-capabilities.ts create mode 100644 demo/ai-todo-assistant/contexts/workspace-settings/features/manage-ai-settings/IMPLEMENTATION_PLAN.md create mode 100644 demo/ai-todo-assistant/contexts/workspace-settings/features/manage-ai-settings/backend/feature.ts create mode 100644 demo/ai-todo-assistant/contexts/workspace-settings/features/manage-ai-settings/contracts/input.schema.json create mode 100644 demo/ai-todo-assistant/contexts/workspace-settings/features/manage-ai-settings/contracts/output.schema.json create mode 100644 demo/ai-todo-assistant/contexts/workspace-settings/features/manage-ai-settings/feature.definition.json create mode 100644 demo/ai-todo-assistant/contexts/workspace-settings/features/manage-ai-settings/frontend/contribution.tsx create mode 100644 demo/ai-todo-assistant/contexts/workspace-settings/features/manage-ai-settings/manifest.ts create mode 100644 demo/ai-todo-assistant/core/architecture/check-bundle-size.mjs create mode 100644 demo/ai-todo-assistant/core/architecture/copy-server-assets.mjs create mode 100644 demo/ai-todo-assistant/core/backend/auth/actor-resolver.ts create mode 100644 demo/ai-todo-assistant/core/backend/config.ts create mode 100644 demo/ai-todo-assistant/core/backend/database/mongo.ts create mode 100644 demo/ai-todo-assistant/core/backend/database/transaction-runner.ts create mode 100644 demo/ai-todo-assistant/core/backend/discovery/context-infrastructure.ts create mode 100644 demo/ai-todo-assistant/core/backend/discovery/discover-features.ts create mode 100644 demo/ai-todo-assistant/core/backend/http/authorization-middleware.ts create mode 100644 demo/ai-todo-assistant/core/backend/http/request-handler.ts create mode 100644 demo/ai-todo-assistant/core/backend/http/request-parser.ts create mode 100644 demo/ai-todo-assistant/core/backend/http/route-matcher.ts create mode 100644 demo/ai-todo-assistant/core/backend/idempotency/canonical-fingerprint.ts create mode 100644 demo/ai-todo-assistant/core/backend/idempotency/mongo-idempotency.ts create mode 100644 demo/ai-todo-assistant/core/backend/mcp-http.ts create mode 100644 demo/ai-todo-assistant/core/backend/mcp-server.ts create mode 100644 demo/ai-todo-assistant/core/backend/mcp.ts create mode 100644 demo/ai-todo-assistant/core/backend/outbox/mongo-outbox.ts create mode 100644 demo/ai-todo-assistant/core/backend/outbox/outbox-publisher.ts create mode 100644 demo/ai-todo-assistant/core/backend/outbox/worker.ts create mode 100644 demo/ai-todo-assistant/core/backend/runtime/aifa-runtime.ts create mode 100644 demo/ai-todo-assistant/core/backend/runtime/mongo-platform-capabilities.ts create mode 100644 demo/ai-todo-assistant/core/backend/server.ts create mode 100644 demo/ai-todo-assistant/core/backend/telemetry/logger.ts create mode 100644 demo/ai-todo-assistant/core/frontend/App.tsx create mode 100644 demo/ai-todo-assistant/core/frontend/FrontendRuntimeProvider.tsx create mode 100644 demo/ai-todo-assistant/core/frontend/api.ts create mode 100644 demo/ai-todo-assistant/core/frontend/app-model.ts create mode 100644 demo/ai-todo-assistant/core/frontend/auth/oidc.ts create mode 100644 demo/ai-todo-assistant/core/frontend/discovery/discover-contributions.ts create mode 100644 demo/ai-todo-assistant/core/frontend/events/FrontendEventProvider.tsx create mode 100644 demo/ai-todo-assistant/core/frontend/events/event-registry.ts create mode 100644 demo/ai-todo-assistant/core/frontend/main.tsx create mode 100644 demo/ai-todo-assistant/core/frontend/query/query-client.ts create mode 100644 demo/ai-todo-assistant/core/frontend/query/react-query.tsx create mode 100644 demo/ai-todo-assistant/core/frontend/slots/LazySlot.tsx create mode 100644 demo/ai-todo-assistant/core/frontend/slots/Slot.tsx create mode 100644 demo/ai-todo-assistant/core/frontend/slots/slot-registry.ts create mode 100644 demo/ai-todo-assistant/core/frontend/theme.ts create mode 100644 demo/ai-todo-assistant/core/frontend/vite-env.d.ts create mode 100644 demo/ai-todo-assistant/core/shared/aifa.ts create mode 100644 demo/ai-todo-assistant/core/shared/feature-manifest.ts create mode 100644 demo/ai-todo-assistant/diagrams/aifa-backend-extension.png create mode 100644 demo/ai-todo-assistant/diagrams/aifa-backend-extension.svg create mode 100644 demo/ai-todo-assistant/diagrams/aifa-frontend-ui-slots.png create mode 100644 demo/ai-todo-assistant/diagrams/aifa-frontend-ui-slots.svg create mode 100644 demo/ai-todo-assistant/diagrams/aifa-system-overview.png create mode 100644 demo/ai-todo-assistant/diagrams/aifa-system-overview.svg create mode 100644 demo/ai-todo-assistant/docker-compose.yml create mode 100644 demo/ai-todo-assistant/index.html create mode 100644 demo/ai-todo-assistant/public/favicon.svg create mode 100644 demo/ai-todo-assistant/public/readme-preview.jpg create mode 100644 demo/ai-todo-assistant/public/readme-workspace.jpg create mode 100644 demo/ai-todo-assistant/tests/ai-provider.test.ts create mode 100644 demo/ai-todo-assistant/tests/built-server.smoke.mjs create mode 100644 demo/ai-todo-assistant/tests/core-quality.test.ts create mode 100644 demo/ai-todo-assistant/tests/features.test.ts create mode 100644 demo/ai-todo-assistant/tests/frontend-accessibility.test.tsx create mode 100644 demo/ai-todo-assistant/tests/frontend-runtime.test.ts create mode 100644 demo/ai-todo-assistant/tests/http-boundary.test.ts create mode 100644 demo/ai-todo-assistant/tests/mongo.integration.test.ts create mode 100644 demo/ai-todo-assistant/tsconfig.server.json create mode 100644 demo/ai-todo-assistant/vite.config.ts create mode 100644 package-lock.json diff --git a/.github/workflows/ai-todo-assistant.yml b/.github/workflows/ai-todo-assistant.yml new file mode 100644 index 0000000..aa10d66 --- /dev/null +++ b/.github/workflows/ai-todo-assistant.yml @@ -0,0 +1,27 @@ +name: AI Todo Assistant + +on: + pull_request: + paths: ["demo/ai-todo-assistant/**"] + push: + paths: ["demo/ai-todo-assistant/**"] + +jobs: + verify: + runs-on: ubuntu-latest + defaults: + run: + working-directory: demo/ai-todo-assistant + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: 22 + cache: npm + cache-dependency-path: demo/ai-todo-assistant/package-lock.json + - run: npm ci + - run: npm run check + - run: npm test + - run: npm run test:integration + - run: npm run build + - run: npm run test:smoke diff --git a/.gitignore b/.gitignore index cf5ff70..2e36f55 100644 --- a/.gitignore +++ b/.gitignore @@ -21,5 +21,7 @@ coverage/ # OS and editor files .DS_Store .idea/ -.vscode/ +.vscode/* +!.vscode/extensions.json +!.vscode/settings.json *.swp diff --git a/.prettierrc.json b/.prettierrc.json new file mode 100644 index 0000000..de753c5 --- /dev/null +++ b/.prettierrc.json @@ -0,0 +1,3 @@ +{ + "printWidth": 100 +} diff --git a/.vscode/extensions.json b/.vscode/extensions.json new file mode 100644 index 0000000..c83e263 --- /dev/null +++ b/.vscode/extensions.json @@ -0,0 +1,3 @@ +{ + "recommendations": ["esbenp.prettier-vscode"] +} diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..a926056 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,7 @@ +{ + "editor.defaultFormatter": "esbenp.prettier-vscode", + "editor.formatOnSave": true, + "editor.formatOnPaste": true, + "files.autoSave": "afterDelay", + "files.autoSaveDelay": 1000 +} diff --git a/demo/ai-todo-assistant/.env.example b/demo/ai-todo-assistant/.env.example new file mode 100644 index 0000000..959851e --- /dev/null +++ b/demo/ai-todo-assistant/.env.example @@ -0,0 +1,17 @@ +NODE_ENV=development +PORT=3000 +HOST=127.0.0.1 +MONGODB_URI=mongodb://127.0.0.1:27017/?replicaSet=rs0 +MONGODB_DATABASE=ai_todo_assistant +OLLAMA_BASE_URL=http://127.0.0.1:11434 +OLLAMA_MODEL=llama3.2 +OIDC_ISSUER_URL= +OIDC_AUDIENCE= +OIDC_JWKS_URL= +# Production MCP calls use this OIDC access token. Never commit a real token. +MCP_ACCESS_TOKEN= +IDEMPOTENCY_LEASE_MS=300000 +DEMO_TENANT_ID=local-tenant +DEMO_USER_ID=local-user +# Optional authenticated SSE endpoint for domain events created outside this browser. +VITE_EVENT_STREAM_URL= diff --git a/demo/ai-todo-assistant/AGENTS.md b/demo/ai-todo-assistant/AGENTS.md index c989d39..296483d 100644 --- a/demo/ai-todo-assistant/AGENTS.md +++ b/demo/ai-todo-assistant/AGENTS.md @@ -2,6 +2,8 @@ > **Required reading:** Follow [ARCHITECTURE.md](ARCHITECTURE.md) before adding or changing any code, feature definition, domain rule, capability, route, MCP tool, or slot contribution. It is the authoritative architecture document; this file is the contributor working agreement. +> **Progress tracking:** Update [TODO.md](TODO.md) when a scoped project task is completed, added, split, or reprioritized. + ## Purpose This app is a production-style demonstration of **AI Atomic Feature Architecture (AIFA)**. diff --git a/demo/ai-todo-assistant/AIFA-Architecture-Diagrams.md b/demo/ai-todo-assistant/AIFA-Architecture-Diagrams.md new file mode 100644 index 0000000..fca2541 --- /dev/null +++ b/demo/ai-todo-assistant/AIFA-Architecture-Diagrams.md @@ -0,0 +1,378 @@ +# AIFA Architecture Diagrams + +This document explains how AI Atomic Feature Architecture (AIFA) makes an application easy to extend on both the backend and frontend. + +## 1. Complete architecture + +The feature contract is the stable center of the architecture. Backend transports, infrastructure adapters, frontend contributions, and the application shell are replaceable parts around it. + +```mermaid +flowchart LR + subgraph Clients[Clients and entry points] + Browser[Browser UI] + ExternalAI[External AI client] + APIClient[HTTP API client] + end + + subgraph Frontend[Frontend composition] + Shell[Application shell] + SlotRegistry[Typed slot registry] + FeatureUI[Feature UI contribution] + + Shell -->|publishes named slots| SlotRegistry + SlotRegistry -->|renders contribution| FeatureUI + end + + subgraph FeatureSlice[Atomic feature slice] + Definition[feature.definition.json] + Contracts[Input and output contracts] + BackendFeature[Backend feature] + FrontendContribution[Frontend contribution] + Manifest[Executable manifest] + + Definition --- Contracts + Definition --- Manifest + Manifest --- BackendFeature + Manifest --- FrontendContribution + end + + subgraph Backend[Governed backend] + HTTP[HTTP adapter] + MCP[MCP adapter] + Runtime[AIFA runtime] + Capabilities[Tenant-bound capabilities] + + HTTP --> Runtime + MCP --> Runtime + Runtime -->|grants declared operations| Capabilities + end + + subgraph Infrastructure[Replaceable infrastructure] + Database[(MongoDB)] + AIProvider[AI provider] + Identity[Identity and authorization] + Audit[Audit log] + Outbox[Event outbox] + end + + Browser --> Shell + FeatureUI -->|calls the published contract| HTTP + ExternalAI --> MCP + APIClient --> HTTP + + FrontendContribution --> FeatureUI + BackendFeature --> Runtime + Runtime --> BackendFeature + + Capabilities --> Database + Capabilities --> AIProvider + Capabilities --> Identity + Capabilities --> Audit + Capabilities --> Outbox + + classDef feature fill:#322a67,stroke:#9c8cff,color:#ffffff,stroke-width:2px; + classDef frontend fill:#123443,stroke:#63d9ff,color:#ffffff; + classDef backend fill:#282442,stroke:#9c8cff,color:#ffffff; + classDef infrastructure fill:#17362f,stroke:#5ce0b6,color:#ffffff; + + class Definition,Contracts,BackendFeature,FrontendContribution,Manifest feature; + class Shell,SlotRegistry,FeatureUI frontend; + class HTTP,MCP,Runtime,Capabilities backend; + class Database,AIProvider,Identity,Audit,Outbox infrastructure; +``` + +### Why this is easy to extend + +- The feature owns one understandable change surface. +- HTTP and MCP reuse the same feature operation. +- Features request capabilities instead of importing infrastructure. +- The application shell composes UI without importing product features. +- Contracts provide a shared language for developers, runtime validation, and AI agents. + +## 2. Backend extension flow + +Adding a backend capability means adding a local feature slice. Generic platform code discovers, validates, and exposes it. + +```mermaid +flowchart LR + subgraph NewFeature[New feature folder] + FD[Feature definition] + IC[Input contract] + OC[Output contract] + BF[Backend behavior] + FM[Feature manifest] + + FD --> IC + FD --> OC + FD --> FM + FM --> BF + end + + subgraph Composition[Generic composition] + Discover[Discover manifests] + Validate[Validate contracts and dependencies] + Register[Register route and MCP tool] + + Discover --> Validate --> Register + end + + subgraph Invocation[Governed invocation] + Authenticate[Resolve actor] + ValidateInput[Validate input] + CheckPolicy[Check scopes and command policy] + Execute[Execute feature] + ValidateOutput[Validate output] + + Authenticate --> ValidateInput + ValidateInput --> CheckPolicy + CheckPolicy --> Execute + Execute --> ValidateOutput + end + + subgraph Grants[Runtime capability grants] + Persistence[Persistence] + Assistant[AI provider] + Clock[Clock and identifiers] + Auditing[Audit and events] + Transaction[Transaction and idempotency] + end + + NewFeature --> Discover + Register --> Authenticate + Execute --> Persistence + Execute --> Assistant + Execute --> Clock + Execute --> Auditing + Execute --> Transaction + + classDef feature fill:#322a67,stroke:#9c8cff,color:#ffffff; + classDef platform fill:#172735,stroke:#63d9ff,color:#ffffff; + classDef runtime fill:#282442,stroke:#9c8cff,color:#ffffff; + classDef capability fill:#17362f,stroke:#5ce0b6,color:#ffffff; + + class FD,IC,OC,BF,FM feature; + class Discover,Validate,Register platform; + class Authenticate,ValidateInput,CheckPolicy,Execute,ValidateOutput runtime; + class Persistence,Assistant,Clock,Auditing,Transaction capability; +``` + +### Backend benefits + +- **Low coupling:** features depend on capability contracts, not MongoDB, HTTP, MCP, or provider SDKs. +- **Replaceable infrastructure:** adapters can change without rewriting business behavior. +- **Consistent security:** HTTP and MCP pass through the same authorization and tenancy boundary. +- **Focused tests:** a feature can be tested with small capability fakes. +- **Safer commands:** idempotency, confirmation, concurrency, audit, and event requirements are explicit. +- **No route-list maintenance:** discovery composes new features automatically. + +## 3. Frontend extension through UI slots + +The application shell owns layout and publishes named extension points. Feature modules provide UI contributions for those slots. + +```mermaid +flowchart LR + subgraph Contributions[Feature-owned UI contributions] + Planner[Generate Task Plan UI] + Composer[Create Task UI] + Status[Change Status UI] + Delete[Delete Task UI] + Settings[AI Settings UI] + end + + subgraph Registry[Typed slot registry] + DiscoverUI[Discover contributions] + TypeCheck[Check slot model] + Order[Order contributions] + Render[Render matching contributions] + + DiscoverUI --> TypeCheck --> Order --> Render + end + + subgraph Shell[Application shell] + HeaderSlot[AppHeader slot] + NavigationSlot[AppNavigation slot] + ContentSlot[AppContent slot] + AssistantSlot[AssistantPanel slot] + ComposerSlot[TaskComposer slot] + RowActionsSlot[TaskRowActions slot] + SettingsSlot[SettingsPanel slot] + end + + Planner -->|AssistantPanel| DiscoverUI + Composer -->|TaskComposer| DiscoverUI + Status -->|TaskRowActions| DiscoverUI + Delete -->|TaskRowActions| DiscoverUI + Settings -->|AppNavigation, AppContent, SettingsPanel| DiscoverUI + + Render --> HeaderSlot + Render --> NavigationSlot + Render --> ContentSlot + Render --> AssistantSlot + Render --> ComposerSlot + Render --> RowActionsSlot + Render --> SettingsSlot + + classDef contribution fill:#322a67,stroke:#9c8cff,color:#ffffff; + classDef registry fill:#172735,stroke:#63d9ff,color:#ffffff; + classDef slot fill:#123443,stroke:#63d9ff,color:#ffffff,stroke-dasharray:5 5; + + class Planner,Composer,Status,Delete,Settings contribution; + class DiscoverUI,TypeCheck,Order,Render registry; + class HeaderSlot,NavigationSlot,ContentSlot,AssistantSlot,ComposerSlot,RowActionsSlot,SettingsSlot slot; +``` + +### UI-slot benefits + +- **Stable shell:** global layout does not need to know which product features exist. +- **Independent feature UI:** each feature owns its view, interaction, and local state. +- **Typed extension points:** each slot exposes the smallest model needed by its contributors. +- **Deterministic composition:** contribution order comes from metadata rather than import order. +- **Easy replacement:** a contribution can be removed or replaced without redesigning the shell. +- **Parallel development:** teams and AI agents work in separate feature folders with fewer merge conflicts. +- **Focused UI testing:** contributions can be tested against their slot model independently. + +## 4. Anatomy of one feature module + +Backend behavior, frontend contribution, executable registration, contracts, and implementation guidance remain colocated. + +```mermaid +flowchart TB + FeatureFolder[Feature folder] + + FeatureFolder --> Definition[feature.definition.json] + FeatureFolder --> Manifest[manifest.ts] + FeatureFolder --> Contracts[contracts] + FeatureFolder --> Backend[backend] + FeatureFolder --> Frontend[frontend] + FeatureFolder --> Plan[IMPLEMENTATION_PLAN.md] + + Contracts --> Input[input.schema.json] + Contracts --> Output[output.schema.json] + + Backend --> Behavior[feature.ts] + Backend --> BackendTests[feature tests] + + Frontend --> Contribution[contribution.tsx] + Frontend --> FrontendTests[UI tests] + + Definition -. declares .-> Manifest + Input -. validates .-> Behavior + Behavior -. returns .-> Output + Manifest -. registers .-> Behavior + Manifest -. registers .-> Contribution + + classDef root fill:#322a67,stroke:#9c8cff,color:#ffffff,stroke-width:2px; + classDef contract fill:#17362f,stroke:#5ce0b6,color:#ffffff; + classDef implementation fill:#172735,stroke:#63d9ff,color:#ffffff; + classDef metadata fill:#282442,stroke:#9c8cff,color:#ffffff; + + class FeatureFolder root; + class Contracts,Input,Output contract; + class Backend,Behavior,BackendTests,Frontend,Contribution,FrontendTests implementation; + class Definition,Manifest,Plan metadata; +``` + +## 5. Same feature across browser and AI clients + +Browser actions and AI tool calls must not create separate business-logic paths. + +```mermaid +sequenceDiagram + autonumber + actor Person + participant UI as Feature UI + participant HTTP as HTTP adapter + actor Agent as AI client + participant MCP as MCP adapter + participant Runtime as AIFA runtime + participant Feature as Atomic feature + participant Capability as Bound capabilities + participant Data as Database, audit, outbox + + alt Browser invocation + Person->>UI: Submit feature action + UI->>HTTP: Contract input + HTTP->>Runtime: Actor + input + metadata + else AI invocation + Agent->>MCP: Invoke typed tool + MCP->>Runtime: Actor + input + metadata + end + + Runtime->>Runtime: Validate contract and policy + Runtime->>Feature: Execute with declared capabilities + Feature->>Capability: Request governed operation + Capability->>Data: Tenant-bound transaction + Data-->>Capability: Persisted result + Capability-->>Feature: Domain result + Feature-->>Runtime: Typed feature result + Runtime-->>HTTP: Validated output + Runtime-->>MCP: Validated output +``` + +### Cross-channel benefits + +- Business behavior exists once. +- Browser and AI clients receive the same validation and error semantics. +- AI tools cannot bypass authorization, tenancy, confirmation, or auditing. +- New transports can be added as adapters without duplicating feature logic. +- Contract parity can be verified automatically in tests. + +## 6. Why AIFA is AI-friendly + +```mermaid +mindmap + root((AI-friendly architecture)) + Local context + One feature folder + Explicit business need + Focused implementation plan + Machine-readable boundaries + JSON Schema contracts + Declared capabilities + Declared dependencies + Declared slots and events + Safer automated changes + Architecture validation + Import rules + Runtime capability checks + Contract tests + Parallel delivery + Fewer shared files + Smaller review surface + Independent tests + Reduced merge conflicts + Explainable composition + Dynamic discovery + Same HTTP and MCP behavior + Typed UI slots + Versioned public contracts +``` + +## 7. Summary of benefits + +| Benefit | Architectural mechanism | Practical result | +|---|---|---| +| Easy extension | Feature-local manifests and discovery | Add a feature without editing central product registries | +| Low coupling | Capability injection and bounded contexts | Change infrastructure without changing business behavior | +| Frontend composability | Named, typed UI slots | Add feature UI without rewriting the app shell | +| Independent testing | Small feature contexts and capability fakes | Fast, focused behavioral tests | +| Cross-channel consistency | Shared AIFA invocation for HTTP and MCP | Browser and AI clients follow the same rules | +| Safer AI development | Machine-readable definitions and contracts | Agents can reason locally with fewer hidden assumptions | +| Parallel work | Colocated feature slices | Fewer shared-file edits and merge conflicts | +| Replaceable adapters | Runtime-owned infrastructure | Swap data, AI, identity, or transport implementations | +| Strong governance | Validation, scopes, tenancy, audit, and events | Extension does not weaken platform safety | +| Long-term evolvability | Versioned contracts and explicit dependencies | Contexts can change without implementation coupling | + +## Required guardrails + +AIFA provides these benefits only when its boundaries are executable: + +1. Validate input, output, provider responses, and events at runtime. +2. Bind actor and tenant inside infrastructure capabilities. +3. Grant only the capabilities declared by the feature. +4. Enforce dependency rules in CI. +5. Type every UI slot through a slot-to-model map. +6. Check feature definitions against executable manifests and contributions. +7. Test HTTP and MCP parity. +8. Keep audit, outbox, business persistence, and idempotency transactionally reliable. + diff --git a/demo/ai-todo-assistant/ARCHITECTURE-REMEDIATION-TODO.md b/demo/ai-todo-assistant/ARCHITECTURE-REMEDIATION-TODO.md new file mode 100644 index 0000000..0bd828c --- /dev/null +++ b/demo/ai-todo-assistant/ARCHITECTURE-REMEDIATION-TODO.md @@ -0,0 +1,158 @@ +# Architecture Remediation Progress + +Updated: 2026-08-15 + +Source review: [Architecture-Improvements.md](Architecture-Improvements.md) + +Status legend: + +- `[x]` implemented and covered by the current quality gates +- `[~]` materially improved, with listed follow-up work remaining +- `[ ]` not completed yet + +## P0 correctness and security boundaries + +- [x] **P0-01 — Enforce feature contracts at runtime.** + - Feature discovery compiles input, output, and event validators with Ajv. + - HTTP rejects invalid input with field details. + - Successful outputs are validated before serialization. + - Task features return tenant-free `TaskView` DTOs rather than domain records. + - Tests prove invalid enums are rejected and `tenantId` fails the output contract. + +- [x] **P0-02 — Make capability enforcement type-safe.** + - Runtime preflights every declared capability. + - Missing capabilities fail the invocation before feature execution. + - Undeclared access throws a private runtime error that is mapped to a typed failure. + - Features use exact `Pick<...>` capability types. + - Negative tests cover missing and undeclared capability access. + +- [x] **P0-03 — Bind tenancy and actor identity in infrastructure.** + - Task and plan persistence adapters are created for an authenticated actor. + - Feature-supplied persistence commands no longer accept tenant or actor IDs. + - Neutral create/update commands are enriched inside MongoDB adapters. + - Integration tests prove a malicious tenant override is ignored. + +- [x] **P0-04 — Make idempotency atomic with mutations.** + - Idempotency completion now commits in the same MongoDB transaction as business data and events. + - Execution fencing prevents an expired worker from completing a recovered command. + - External AI calls are prepared before opening the MongoDB transaction. + - The durable command stores its canonical input before the provider call and persists prepared external-capability results for crash recovery. + - Lease heartbeats and execution fences protect external work; integration tests cover recovery before preparation, after preparation, and before completion. + +- [x] **P0-05 — Persist contract-conforming events.** + - Features emit typed event intent through `DomainEventEmit`. + - Runtime-owned infrastructure adds the full actor/request/event envelope. + - Events are validated against the feature's declared schema before persistence. + - Outbox delivery metadata is stored separately from the event payload. + - Integration tests validate actual stored audit and outbox event payloads. + +- [x] **P0-06 — Make outbox claiming concurrency-safe.** + - Claims use atomic `findOneAndUpdate` operations. + - Claims carry worker IDs, claim tokens, and expiring leases. + - Only the winning worker receives an event. + - Expired processing leases are recoverable. + - Failures use bounded exponential backoff and a dead-letter state. + - The worker runs as a supervised loop and handles SIGINT/SIGTERM. + - Integration tests cover concurrent claims and expired-lease recovery. + +- [x] **P0-07 — Remove hidden product coupling from core.** + - Context infrastructure is dynamically discovered through infrastructure manifests. + - The server no longer imports Task Management or AI Planning adapters directly. + - Generic clock, ID, event, outbox, and transaction behavior is core-owned. + - Task taxonomy moved to Task Management's versioned contract. + - Task state and commands live in a Task Management controller; the core shell now owns navigation and authentication only. + +## P1 architectural integrity + +- [x] **P1-01 — Prevent definition/manifest/schema/contribution drift.** + - Discovery checks name, route, method, capabilities, scopes, MCP exposure, tool name, and slots. + - The architecture validator compares declared frontend slots with executable contribution source. + - Manifests use `satisfies FeatureManifest`. + +- [x] **P1-02 — Make dependency enforcement scalable.** + - Dependency rules are generated from discovered contexts and features. + - Rules cover cross-context implementation imports, feature-to-feature imports, infrastructure direction, and domain direction. + +- [x] **P1-03 — Type slots by slot name.** + - `SlotModelMap` binds every slot name to its model at compile time. + - `SlotContribution` and `Slot` preserve that relationship. + - Registry instances reject duplicate contribution names and support test reset. + - Slot models are small, purpose-specific shell, composer, row-action, assistant, and task-list interfaces. + +- [x] **P1-04 — Remove the central feature-name edit bottleneck.** + - `FeatureName` is generated from discovered feature definitions. + +- [x] **P1-05 — Make MCP tools safe contract projections.** + - MCP input is projected from each feature's JSON Schema. + - Mutations require a stable caller-supplied `commandId` and forward it as idempotency metadata. + - Generic route-template expansion replaces the task-specific path replacement. + - Tool descriptions and safety annotations include read-only, destructive, confirmation, and idempotency intent. + - `zod` is now a direct dependency. + - Remote stateless Streamable HTTP MCP exposes OAuth protected-resource metadata and scope-filtered tools. + - An in-memory MCP client test proves route expansion, actor forwarding, body projection, and idempotency parity with HTTP. + +- [x] **P1-06 — Report AI behavior honestly.** + - Provider failures and invalid output are typed failures; there is no silent fabricated fallback. + - Ollama uses structured output, schema validation, timeouts, bounded retry, and response-size limits. + - Successful plans contain provider, model, prompt-version, and latency provenance. + - The adapter enforces concurrent and per-minute limits, opens a bounded circuit, and reports readiness health metrics. + - Dedicated tests cover retry, invalid output, circuit opening, concurrency limiting, and rate limiting. + +- [x] **P1-07 — Remove disconnected AI settings behavior.** + - The browser no longer stores pretend provider settings in local storage. + - The UI accurately states that provider configuration is server-managed. + +- [x] **P1-08 — Align domain behavior with acceptance criteria.** + - Status transitions use an explicit transition table and typed invalid-transition failure. + - Delete distinguishes not-found from version conflict. + - Optimistic save conflicts reload and return a sanitized current representation. + - Event types use an enum. + +- [x] **P1-09 — Harden HTTP behavior.** + - JSON requests enforce content type and a one-megabyte size limit. + - A top-level error boundary returns sanitized typed errors. + - Correlation and causation headers are preserved or derived. + - CORS and API base URL are configurable. + - Development identity headers are excluded from production browser builds. + - The browser implements OIDC authorization-code/PKCE with state validation and session-scoped access tokens. + - Request deadlines, structured completion/error telemetry, readiness detail, and production status mapping are implemented. + +- [x] **P1-10 — Define a deployable backend path.** + - Zero-config development uses `MongoMemoryReplSet`. + - The build emits server and web artifacts separately. + - Production `start`, `start:mcp`, and `start:outbox` scripts exist. + - Discovery works with both TypeScript source and compiled JavaScript manifests. + - CI starts the compiled server artifact, waits for Mongo-backed readiness, and shuts it down cleanly. + +## P2 quality and scale + +- [x] Frontend commands reject on failure; create and planning UI advance only after success. +- [x] Busy state is keyed by operation/entity instead of one application-wide flag. +- [x] Filter refreshes ignore stale responses. +- [x] Frontend server state uses semantic cache tags; feature-owned hooks and declared domain-event consumers invalidate independent list, summary, and count subscribers without feature imports. +- [x] Slot registry lifecycle is explicit and duplicate registration fails. +- [x] List Tasks has bounded cursor pagination with deterministic `(createdAt, id)` ordering. +- [x] Configuration validates ports and URLs; liveness and MongoDB readiness are separate endpoints. +- [x] Tests now cover contract boundaries, capability failures, tenant overrides, event schemas, fencing, and concurrent outbox claims. +- [x] Production contributions are code-split and MUI icons use direct imports. + - The build now transforms about 957 modules; the largest chunk is about 358 kB. + - CI enforces a 400 kB largest-chunk and 600 kB total-JavaScript budget. +- [x] Generated contracts are used by frontend DTOs and selected feature inputs/outputs; runtime schemas remain authoritative. + - Schema titles produce stable names, and generation deduplicates identical declarations while rejecting conflicts. + +## P3 interface polish + +- [x] Delete confirmation uses an accessible MUI dialog instead of `window.confirm`. +- [x] Create and planning selects have accessible labels. +- [x] Priority comparisons use the enum rather than a raw string. +- [x] Repeated brand, navigation, status, and category visual constants are centralized with the MUI theme. +- [x] Focused server-rendered accessibility tests cover task inputs and destructive-action naming. +- [x] Browser verification covers landing-to-workspace composition, task creation, refresh, and the accessible delete confirmation. + +## Verification status + +- [x] `npm run check` +- [x] `npm test` — 18 tests passing +- [x] `npm run build` — server and web artifacts produced +- [x] Built-server smoke test +- [x] Browser end-to-end verification diff --git a/demo/ai-todo-assistant/ARCHITECTURE.md b/demo/ai-todo-assistant/ARCHITECTURE.md index aace84c..cf97245 100644 --- a/demo/ai-todo-assistant/ARCHITECTURE.md +++ b/demo/ai-todo-assistant/ARCHITECTURE.md @@ -93,6 +93,12 @@ Every request—HTTP, MCP, browser, or provider callback—must resolve to an `A Browser authentication uses an OpenID Connect authorization-code flow with PKCE. The remote MCP endpoint uses Streamable HTTP and acts as an OAuth 2.1 protected resource: it validates bearer-token audience, expiry, tenant, user, and scopes on every request. A local stdio MCP connection obtains its credentials from the local environment and is restricted to the configured tenant. Token revocation, client registration, and scope assignment are owned by the identity adapter in core. +### Authorization Middleware + +Authorization is a core HTTP middleware concern, never feature logic. `createAuthorizationMiddleware` runs after route discovery and before idempotency or feature execution. It resolves the actor once, returns `401 NotAuthorized` when no trusted actor is available, and returns `403 Forbidden` when the actor lacks any scope required by the executable feature manifest. Both HTTP request handlers use this middleware so local API routes and dynamically discovered routes cannot drift. + +`feature.definition.json` declares each feature's `mcpScopes`; the colocated `manifest.ts` binds those enum-backed scopes to the executable route. This keeps the permission requirement visible in the machine-readable plan while allowing the runtime to enforce it without importing a feature's business code. Development-only `x-aifa-*` actor headers are disabled in production; production actors must come from a verified OIDC bearer token. The local stdio MCP bridge forwards its configured development actor or production bearer token through the same middleware. + Tasks, task plans, audits, idempotency records, and outbox events are tenant-owned. MongoDB capability adapters receive the actor and apply `tenantId` to every query and mutation; the feature never supplies or overrides a tenant ID. Required indexes include `{ tenantId, status, category }`, `{ tenantId, createdAt }`, and unique `{ tenantId, actorId, featureName, idempotencyKey }` for commands. MongoDB must run as a replica set (or sharded cluster) in every environment that enables multi-document mutations, including integration tests. This is required for the task mutation, audit, idempotency record, and outbox event to commit atomically. @@ -101,7 +107,7 @@ MCP authentication maps each client connection to an actor and only exposes tool ## Command Safety and Concurrency -Every mutation has a caller-supplied idempotency key and expected task version where relevant. Runtime capabilities persist the command result with the idempotency record, so an HTTP or MCP retry returns the original result without repeating persistence or audit work. +Every mutation has a caller-supplied idempotency key and expected task version where relevant. The idempotency store atomically claims a key before feature execution, records a fenced execution identifier, and persists the command result on completion. A bounded lease lets a retry recover from a crashed worker without allowing an earlier execution to overwrite the recovered result. HTTP or MCP retries return the original completed result without repeating persistence or audit work. Features use typed failures for `VersionConflict`, `IdempotencyKeyReuse`, `NotAuthorized`, and `ConfirmationRequired`. These failures are part of each adapter contract, not raw infrastructure errors. @@ -155,12 +161,98 @@ Frontend discovery uses Vite `import.meta.glob` to load feature manifests and re MCP discovery loads only manifest-declared tools. Each tool calls the same AIFA feature as the HTTP route, so MCP clients cannot bypass validation, authorization, capability checks, or audit records. +## Frontend State, Hooks, and Event Synchronization + +Frontend composition follows the same dependency direction as the backend. Slots compose visual +contributions, feature-local hooks adapt one feature contract to React, and a runtime-owned query +cache synchronizes server-owned state. A feature must not import another feature, call another +feature's hook, hold a reference to another feature's component, or emit an imperative UI event +such as `RefreshTaskCount`. + +Frontend feature code may import generic frontend infrastructure from `core/`, its own feature, +its bounded context's domain, and versioned contracts published by a bounded context. A versioned +frontend contract may define read models, query inputs, and stable cache tags, but it must not +contain React components, hooks, API calls, or feature orchestration. + +### Responsibilities + +- **Components** render a slot contribution and own only transient presentation state such as an + open menu, draft field value, or selected tab. +- **Feature-local query hooks** read one published read model and expose loading, success, empty, + and typed failure states to that feature's components. +- **Feature-local command hooks** invoke one feature boundary and own command state keyed by + operation and, where relevant, entity ID. +- **Core frontend infrastructure** owns generic query caching, request deduplication, cancellation, + retries, authentication-aware transport, event transport, and provider lifecycle. Core must not + know task, plan, settings, or other product-specific cache keys or event reactions. +- **Versioned bounded-context frontend contracts** define stable read models, query inputs, and + enum-backed cache tags shared by independent feature consumers. They contain no behavior. +- **Frontend domain-event consumers** validate a published event contract and translate that fact + into semantic cache invalidation. They never target a component or call a feature directly. Each + consumer belongs to a feature slice, declares the consumed event schema in `consumesContracts`, + and registers through that feature's discovered frontend manifest; core keeps no hand-written + product-event registry. + +The application installs the generic query and event providers above the app shell so slot +contributions in the header, navigation, content, footer, or future surfaces observe the same +tenant-scoped cache. Cache entries and event subscriptions are scoped by the authenticated actor +and tenant and are cleared when that identity changes or signs out. + +### Semantic Invalidation + +Commands and events coordinate through semantic resource tags rather than component names. For +example, Task Management may publish a versioned `TaskCollection` cache tag. List, board, summary, +and count queries that depend on the task collection associate their cache entries with that tag. +Create, status-change, and delete command hooks invalidate the tag only after an explicit successful +result. The query runtime then marks every dependent entry stale, deduplicates refetches, and +rerenders subscribed contributions. + +Consequently, a Create Task contribution does not know that a task-count contribution exists. Both +depend only on the published Task Management frontend contract and generic core query runtime: + +```txt +Create Task component + -> feature-local create command hook + -> successful Create Task result + -> invalidate TaskCollection + -> generic query cache + -> subscribed task list, board, summary, and count queries update +``` + +Mutation responses are the immediate synchronization mechanism for commands issued in the current +browser. The successful command hook invalidates affected tags without waiting for the durable +outbox event to return to that browser. + +Tasks may also be created by MCP clients, another browser, another actor, or background processing. +For those cases, a generic core SSE or WebSocket adapter delivers validated versioned domain events +to dynamically registered frontend event consumers. A Task Management consumer maps +`TaskCreatedV1`, `TaskStatusChangedV1`, and `TaskDeletedV1` to the same `TaskCollection` +invalidation. Receiving both the local-success invalidation and its later domain event is expected; +invalidation must be idempotent and refetches deduplicated. + +Published events are facts, not client state containers. When an event contains only an aggregate +identifier, consumers invalidate and refetch the authoritative read model rather than constructing +a partial cache entry. Direct cache updates are allowed only when the event or command result +contains the complete versioned read model required by every affected query and preserves tenant, +filter, ordering, authorization, and optimistic-concurrency semantics. + +### Frontend Boundary Tests + +Focused frontend tests must verify that: + +1. a command hook invalidates its declared semantic tags only after success; +2. a failed command leaves dependent cached data intact and exposes its typed failure; +3. each domain-event consumer accepts its declared version and maps it to the correct tags; +4. duplicate local and remote invalidations are harmless and request work is deduplicated; +5. independently mounted slot contributions observe the same cache without importing each other; +6. actor or tenant changes clear cached data and terminate the previous event subscription. + ## Dependency Boundaries ```txt core ────────────────────────────► core only context domain ──────────────────► core + its own domain -feature ─────────────────────────► core + its own domain + itself +feature ─────────────────────────► core + itself + own domain + versioned context contracts adapter (HTTP/MCP/UI) ───────────► core + its registered feature contract cross-context work ──────────────► versioned contracts or domain events only ``` @@ -169,6 +261,7 @@ Forbidden dependencies: - core → context or feature; - feature → another feature; +- versioned frontend contract → React, hooks, transport, cache runtime, or feature behavior; - a context → another context’s domain or feature; - direct MongoDB/HTTP/MCP/provider SDK access from a feature; - circular imports. diff --git a/demo/ai-todo-assistant/Architecture-Improvements.md b/demo/ai-todo-assistant/Architecture-Improvements.md new file mode 100644 index 0000000..95eba00 --- /dev/null +++ b/demo/ai-todo-assistant/Architecture-Improvements.md @@ -0,0 +1,582 @@ +# Architecture and Technical Review + +Reviewed: 2026-08-15 + +Scope: `demo/ai-todo-assistant`, including the AIFA runtime, feature discovery, HTTP and MCP adapters, bounded-context boundaries, MongoDB persistence, outbox/idempotency behavior, React composition, typed slots, AI-provider integration, contracts, tests, and build/deployment setup. + +## Executive summary + +The repository has a good architectural vocabulary: bounded contexts, feature-local definitions, capability injection, JSON Schema contracts, optimistic concurrency, an outbox, and frontend slots are all appropriate choices. The documentation is also clearer than the implementation of most demo applications. + +It is not yet a best-practice reference implementation, however. Several of its strongest guarantees exist only in documentation or JSON files and are not enforced by the executable path. The largest risks are: + +1. HTTP and MCP input/output schemas are not used at runtime. +2. Features choose tenant IDs instead of receiving tenant-bound capabilities. +3. Idempotency is not atomic with the business transaction and has a duplicate-execution crash window. +4. Written audit/outbox records do not conform to the declared event contracts. +5. The outbox claim operation is unsafe with concurrent workers. +6. `core` is coupled to product contexts on both the backend and frontend despite the stated dependency rule. +7. Capability enforcement can return a `FeatureFailure` where a feature expects a domain value, causing silent data corruption rather than a safe failure. +8. The frontend slots are dynamically discovered but are not actually typed per slot, and every contribution depends on one product-wide `AppModel`. +9. MCP exposes an unstructured catch-all schema and creates a new idempotency key on every call, which is poor for AI tool safety and retry behavior. +10. The Ollama path silently replaces provider failures or invalid output with fabricated suggestions while recording the operation as a successful AI plan. + +The current checks and tests pass, and the frontend production bundle builds. That proves the current implementation is internally consistent at the TypeScript/import-test level; it does not prove the architectural claims above. + +## Severity guide + +- **P0 — correctness or security boundary:** fix before presenting this as a best-practice architecture. +- **P1 — architectural integrity:** fix before using the repository as the basis for extension. +- **P2 — maintainability and operational quality:** address as the reference implementation matures. +- **P3 — polish:** valuable, but not a structural blocker. + +## Findings + +### P0-01 — Declared JSON Schemas are not enforced at runtime + +**Evidence** + +- `core/backend/http/request-handler.ts:49-85` parses an arbitrary JSON object and passes it directly to a feature. +- `core/backend/mcp.ts:13` uses `z.object({}).catchall(z.unknown())` for every tool. +- `core/architecture/validate-feature-definitions.ts:92-166` validates that schemas compile and exist, but does not build runtime validators. +- `core/shared/generated/contracts.ts` is generated, but no application code imports it. + +**What is wrong and why** + +The feature definitions claim that the same schema governs HTTP, MCP, TypeScript, and provider boundaries. In practice, TypeScript interfaces are handwritten and untrusted runtime values are cast into them. Invalid enums, overlong strings, missing fields, and additional properties can reach feature logic. This also makes the API behavior differ from the published contract. + +The mismatch is already observable: task features return the domain `Task`, including `tenantId`, while the output schema references `TaskView`, which explicitly excludes `tenantId` (`contexts/task-management/contracts/v1/task-view.schema.json:7-28`). The server serializes the actual domain object, so internal tenancy data is exposed and the response violates its schema. + +**Improve** + +- Compile feature-local input and output validators during discovery. +- Store the validators in the executable manifest and validate before and after `runtime.run`. +- Map domain entities to explicit API DTOs; never serialize persistence/domain records directly. +- Generate and import TypeScript types from the same schemas instead of maintaining parallel interfaces. +- Return a typed `InvalidInput` with field-level validation details and treat invalid feature output as an internal contract violation. + +### P0-02 — Capability enforcement fails with the wrong value type + +**Evidence** + +- `core/backend/runtime/aifa-runtime.ts:14-35` returns an async function containing `fail(...)` for undeclared or missing capabilities. +- Feature capability interfaces expect values such as `string`, `Task`, `Task[]`, or `void`, not `FeatureFailure`. +- Each feature is typed against the entire context capability interface, even though it declares only a subset; for example `create-task/backend/feature.ts:15-22` receives `TaskCapabilities`. + +**What is wrong and why** + +If a required capability is missing, `ClockNow()` can return a `FeatureFailure` object where code expects an ISO string, or `TaskLoad()` can return a failure object where code expects a task. The feature can then persist corrupted data or crash later. Likewise, TypeScript does not prevent a feature from calling an undeclared method because it receives the full capability interface. + +This is more dangerous than having no enforcement because it appears to fail safely while preserving an incompatible compile-time type. + +**Improve** + +- Preflight every declared capability before invoking a feature and return one invocation-level typed failure if any are missing. +- Make undeclared access throw a private runtime exception that `run` catches and maps; never masquerade as a capability's return value. +- Type each feature with `Pick` or generate its exact capability shape from the definition. +- Add negative tests for missing and undeclared capability access. + +### P0-03 — Tenant isolation is delegated to feature code + +**Evidence** + +- `contexts/task-management/domain/task-capabilities.ts:5-25` accepts `tenantId` in capability inputs and accepts a complete tenant-owned `Task` for writes. +- `create-task/backend/feature.ts:27-41`, `list-tasks/backend/feature.ts:14-17`, and `delete-task/backend/feature.ts:19-30` copy or pass `context.actor.tenantId` themselves. +- `generate-task-plan/backend/feature.ts:36-48` constructs a tenant-owned plan and audit record. + +**What is wrong and why** + +The documented rule says runtime infrastructure derives tenancy and a feature cannot set or infer it. The implementation does the opposite. A future feature can accidentally or deliberately pass a different tenant ID or reuse a tenant-owned object. Review discipline is the only guard. + +**Improve** + +- Bind persistence, audit, and outbox capabilities to the authenticated actor in the composition/runtime layer. +- Remove `tenantId` and `actorId` from all feature-supplied capability inputs. +- Accept tenant-neutral commands/entities at the feature boundary and add tenancy only in infrastructure adapters. +- Add a test with a deliberately malicious feature that attempts cross-tenant access and prove that the capability API makes it impossible. + +### P0-04 — Idempotency is not atomic with business mutations + +**Evidence** + +- `core/backend/server.ts:65-68` claims an idempotency record before the transaction. +- `core/backend/server.ts:88-91` commits the feature transaction, then completes the idempotency record afterward. +- `core/backend/idempotency/mongo-idempotency.ts:83-91` completes without the business transaction's session. + +**What is wrong and why** + +If the process crashes after the task/audit/outbox transaction commits but before `complete`, the record remains pending. After its lease expires, a retry executes the mutation again. Create and plan operations can produce duplicates because they generate fresh IDs. This directly contradicts the claim that the command result and mutation commit atomically. + +There is a second design issue: every mutation is wrapped in a MongoDB transaction, so `generate-task-plan` performs a potentially 15-second external Ollama call inside a database transaction (`server.ts:88-90`, `ollama-task-planner.ts:33-38`). Transaction retries can call the provider more than once, and long network waits hold database transaction resources. + +**Improve** + +- Model command execution as a durable state machine with the final result committed in the same transaction as business data, audit, and outbox. +- Keep external AI calls outside MongoDB transactions. Persist a request/work item first, call the provider, then transactionally persist the validated result and command completion. +- Add lease heartbeats/fencing where work can exceed the lease. +- Treat the idempotency key as request metadata, not a feature-domain input. Normalize header/body transport before fingerprinting. +- Test crashes at every boundary: after claim, after provider response, after business commit, and before result completion. + +### P0-05 — Persisted events do not match their declared contracts + +**Evidence** + +- `contexts/task-management/infrastructure/mongo-task-capabilities.ts:51-55` writes `{ type, tenantId, actorId, taskId, occurredAt, status }`. +- Declared event schemas require `eventId`, `eventType`, `schemaVersion`, `correlationId`, `causationId`, `idempotencyKey`, and event-specific data. For example, `task-status-changed.schema.json:6-34` also requires previous/current status and version. +- `AuditRecord` accepts an untyped `type: string` (`task-capabilities.ts:20-25`), contrary to the enum rule. + +**What is wrong and why** + +The outbox does not contain the published event shapes, so consumers cannot use the declared schemas. Correlation and causation are lost, schema versioning is nominal, and the status-change event lacks the information its contract promises. Static validation only checks that event schema files exist. + +**Improve** + +- Replace `AuditRecord` with separate typed `AuditRecord` and `DomainEventEmit` capabilities, or a typed unit-of-work result interpreted by the runtime. +- Have the runtime add the standard event envelope from actor and request metadata. +- Make event payloads discriminated unions keyed by an `AuditEventType`/`DomainEventType` enum. +- Validate emitted events against the referenced schema before persistence. +- Add contract tests that read actual MongoDB outbox documents and validate them with Ajv. + +### P0-06 — Outbox claiming can double-deliver and can strand work forever + +**Evidence** + +- `core/backend/outbox/mongo-outbox.ts:10-27` first reads a batch, then independently updates each record, but returns every originally read event even when another worker won the update. +- Only `Pending` and `Failed` are claimable (`:12`); a worker crash leaves `Processing` records permanently stuck. +- `core/backend/outbox/worker.ts:8-12` processes one batch and exits. + +**What is wrong and why** + +Two workers can read the same events and both deliver them. A worker that loses the status update still receives the event from `claim`. Conversely, a crash after setting `Processing` makes the event unrecoverable. This is not a reliable outbox. + +**Improve** + +- Claim one event at a time with atomic `findOneAndUpdate`, a worker ID, fencing token, and lease expiry. +- Return only records actually claimed by that worker. +- Recover expired `Processing` leases and apply bounded exponential backoff plus a dead-letter state. +- Make the worker a supervised loop or explicitly document and schedule the one-shot behavior. +- Test two concurrent workers and crash recovery. + +### P0-07 — `core` has hidden product and cross-context dependencies + +**Evidence** + +- `core/backend/server.ts:24-46` hard-codes imports for Task Management and AI Planning infrastructure. +- `core/frontend/app-model.ts:1-43` imports Task Management domain types and defines task- and AI-specific operations in core. +- `core/shared/task-taxonomy.ts` contains Task Management business vocabulary. +- `contexts/ai-planning/domain/task-planning.ts:1` imports the core task taxonomy rather than the published Task Management contract. +- The Task Management Mongo adapter supplies `IdCreate`, `ClockNow`, and `AuditRecord`; AI Planning relies on those capabilities through object spreading in `server.ts:43-47`. + +**What is wrong and why** + +Adding or removing a context requires editing core, and AI Planning silently depends on Task Management infrastructure. This is the opposite of independently composable feature modules. The current dependency check misses dynamic imports and type-only/context-via-core coupling, so its passing result is misleading. + +**Improve** + +- Move generic clock, ID, audit, outbox, and transaction capabilities into core-owned adapters. +- Discover context infrastructure/provider modules through explicit adapter manifests at the composition root. +- Move task taxonomy to Task Management and generate a versioned consumer type for AI Planning. +- Replace the product-specific core `AppModel` with shell-only contracts plus feature-owned stores/controllers. +- Strengthen dependency-cruiser rules to cover every context pair, infrastructure folders, type-only imports, and dynamic-import literals. Add a direct source scan/test if the tool cannot see dynamic imports. + +### P1-01 — Feature definitions, executable manifests, schemas, and UI exports can drift + +**Evidence** + +- Delete Task declares `TaskLoad` in `feature.definition.json:11`, but its executable feature declares only `TaskDelete` and `AuditRecord` (`backend/feature.ts:13-16`). +- List Tasks declares a `TaskSummary` contribution in its definition and manifest, but exports no `TaskSummary` contribution (`frontend/contribution.tsx:261-293`). +- `validate-feature-definitions.ts` never imports manifests or frontend contributions to compare them with the definition. + +**What is wrong and why** + +There are multiple sources of truth. A green architecture check does not mean the executable application matches the definition. This is especially unfriendly to AI-assisted changes because agents can update one representation and reasonably assume it is authoritative. + +**Improve** + +- Generate the executable manifest's static metadata from `feature.definition.json`, or make a typed TypeScript definition the source and emit JSON. +- At minimum, validate name, route, method, capabilities, scopes, MCP exposure/tool name, and frontend slot/name parity against loaded executable modules. +- Fail on declared-but-missing and implemented-but-undeclared contributions. +- Use `satisfies FeatureManifest` on every manifest to retain literal checking. + +### P1-02 — The dependency policy is incomplete and not scalable + +**Evidence** + +- `core/architecture/.dependency-cruiser.cjs:10-21` names only Task Management and AI Planning cross-context rules; Workspace Settings is absent. +- It does not enforce feature-to-feature isolation within the same context, feature-to-infrastructure imports, or infrastructure-to-feature direction. +- `dependency-policy.json` is descriptive and is not consumed by the checker. + +**What is wrong and why** + +Every new context requires hand-editing pairwise rules, creating an O(n²) policy and a central merge hotspot. Several forbidden dependency classes described in `ARCHITECTURE.md` are not executable rules. + +**Improve** + +- Express generic path-capture rules for context identity and feature identity. +- Enforce allowed dependency directions rather than enumerating a few forbidden pairs. +- Either generate dependency-cruiser configuration from `dependency-policy.json` or remove the unused policy file. +- Add architecture fixture tests containing intentional violations so rule regressions are visible. + +### P1-03 — The slot system is dynamic, but not typed by slot + +**Evidence** + +- `SlotContribution` has no relationship between `slot` and `Model` (`slot-registry.ts:5-10`). +- `Slot` accepts `model: unknown` (`Slot.tsx:5`). +- All feature UIs import the same product-wide `AppModel`, including methods owned by unrelated features. +- Row action contributors locally redeclare an identical `RowModel` instead of consuming a slot contract. + +**What is wrong and why** + +A contribution can register for the wrong slot with the wrong model and still compile. The central `AppModel` couples Create Task, List Tasks, Delete Task, Change Status, AI Planning, navigation, and settings. Removing one feature still leaves its API in core. This is service-locator-style coupling disguised as slots. + +**Improve** + +- Define a `SlotModelMap` and type `SlotContribution` as `render(model: SlotModelMap[S])`. +- Give each slot the smallest capability/view model it needs. +- Put feature state and commands behind feature-owned controllers/hooks; shell slots should receive composition concerns only. +- Define reusable row-action and navigation slot contracts centrally without importing product domain implementations. +- Avoid wrapping every contribution in a `` (`Slot.tsx:8-10`); use keyed fragments or a slot-specific layout component because block MUI components inside spans produce invalid semantics/layout. + +### P1-04 — Central enums create a feature-extension bottleneck + +**Evidence** + +- Every feature and slot name must be added to `core/shared/architecture-enums.ts`. +- The validator then requires each feature definition name and slot to exist in those enums (`validate-feature-definitions.ts:123-129`). + +**What is wrong and why** + +The closed enum is useful for well-known platform concepts, but feature names are discovered extension identifiers. Requiring every new feature to edit a central file contradicts “add one feature folder” and increases merge conflicts for humans and AI agents working in parallel. + +**Improve** + +- Generate `FeatureName` (and, if desired, contribution identifiers) deterministically from validated definitions. +- Keep a small closed enum only for platform-owned slots/capabilities, or support bounded-context-owned slot extension namespaces. +- Add duplicate detection without requiring a hand-maintained registry. + +### P1-05 — MCP tools are not safe, descriptive contract projections + +**Evidence** + +- Every tool receives the same catch-all schema (`core/backend/mcp.ts:13,27-31`). +- Tool descriptions contain only the feature name. +- Mutations receive a newly generated idempotency key when the caller omits one (`:37-45`), so an MCP retry is a new command. +- Route adaptation only knows the literal `:taskId` parameter (`:46-49`). +- `zod` is imported directly but is only a transitive dependency of the MCP SDK, not a declared dependency. + +**What is wrong and why** + +AI clients cannot see required fields, enums, confirmation constraints, or useful descriptions. Invalid tool input travels through the same unvalidated HTTP path. Automatic random keys defeat caller-visible idempotency. The path adapter is not extensible to new route parameters. + +**Improve** + +- Project each feature's JSON Schema into the MCP SDK input schema and include field descriptions/examples. +- Require or deterministically derive a stable command ID supplied by the calling conversation/tool execution; preserve it across retries. +- Surface confirmation requirements in schema and tool annotations, and enforce them at the governed boundary. +- Use a generic route-template expander and shared HTTP/MCP invocation adapter. +- Declare every direct import in `package.json`. +- Add remote Streamable HTTP/OAuth support if the architecture document continues to promise it; otherwise narrow the documentation to the implemented stdio bridge. + +### P1-06 — AI failures are silently reported as successful plans + +**Evidence** + +- `contexts/ai-planning/infrastructure/ollama-task-planner.ts:39-46` returns local fallback suggestions for HTTP failure, invalid JSON, invalid structure, timeouts, and every thrown error. +- The adapter uses manual shape checks rather than the published suggestion schema (`:19-27`). +- The feature always persists and audits `TaskPlanGeneratedV1` (`generate-task-plan/backend/feature.ts:32-51`). +- The feature definition says invalid output returns `AssistantResponseInvalid`, but that error code does not exist and the behavior is not implemented. + +**What is wrong and why** + +Users and operators cannot tell whether AI ran. Provider outages and contract regressions look like successful AI output, corrupting observability and trust. The implementation also omits documented bounded retries, rate limits, health checks, structured-output mode, and response-size limits. + +**Improve** + +- Return a discriminated provider result containing provider/model, provenance, latency, and typed failure. +- Validate output with the same Ajv schema used elsewhere and request Ollama structured JSON output when supported. +- Make fallback an explicit product policy and label fallback suggestions in the result/UI; do not silently substitute it. +- Add bounded retry only for retryable failures, response byte/token limits, circuit breaking/rate limiting, and metrics. +- Store prompt/template version and model provenance with the plan without storing sensitive raw content by default. + +### P1-07 — AI settings are disconnected from runtime behavior + +**Evidence** + +- Settings are stored only in browser local storage (`manage-ai-settings/frontend/contribution.tsx:13-28`). +- The backend feature always returns `{ configured: false }` (`backend/feature.ts:8-17`). +- The server constructs Ollama once from environment configuration (`core/backend/server.ts:38-47`). +- The planner never reads workspace settings. + +**What is wrong and why** + +The UI claims users can choose Ollama or MCP, but changing it has no effect. This is misleading and introduces three conflicting configuration sources: local storage, the placeholder settings endpoint, and server environment variables. + +**Improve** + +- Either remove/label the screen as a non-functional mock or implement a tenant-scoped settings aggregate and provider-router capability. +- Keep credentials server-side; browser settings should reference a server-managed connection ID, not arbitrary runtime endpoints. +- Add a connection test operation, authorization, audit trail, validation, and clear effective-configuration display. +- Include `SettingsRead` in the local UI's scopes if the endpoint is actually used (`core/frontend/api.ts:4-8`). + +### P1-08 — Domain and feature behaviors contradict their own acceptance criteria + +**Evidence** + +- `transitionTaskStatus` permits every enum-to-enum transition (`contexts/task-management/domain/task.ts:21-29`), while the definition promises invalid-transition failures. +- Delete returns `NotFoundOrVersionConflict` (`delete-task/backend/feature.ts:19-25`), while the definition promises `NotFound` and the architecture promises a distinct version conflict with the current representation. +- After a save race, Change Status returns the task loaded before the race as conflict details (`change-task-status/backend/feature.ts:36-40`), not the current stored task. +- Event type literals remain free-form strings rather than enums. + +**What is wrong and why** + +The domain model is not the authoritative source of the rules described by the architecture. Ambiguous errors make clients guess whether to refresh, retry, or report a missing record. + +**Improve** + +- Define an explicit transition table and a typed domain result such as `InvalidStatusTransition`. +- For delete, load then delete with optimistic version inside the transaction, distinguishing not-found from stale-version safely. +- On optimistic conflicts, reload and return a sanitized current view. +- Generate or centralize typed error/event unions and test every transition and race path. + +### P1-09 — HTTP robustness and production authentication are incomplete + +**Evidence** + +- Request bodies are buffered without a size limit (`request-parser.ts:7-10`). +- Exceptions from actor resolution, feature execution, MongoDB, or serialization are not caught by the request handler (`request-handler.ts:33-89`). +- The frontend hard-codes the API origin and development identity headers (`core/frontend/api.ts:1-10`). +- CORS origin is hard-coded in the backend (`server.ts:51-54`). +- `statusFor` maps most typed failures to 400 and does not map `NotFoundOrVersionConflict` meaningfully (`request-handler.ts:21-31`). + +**What is wrong and why** + +Large requests can exhaust memory, infrastructure errors can terminate a request without a stable JSON response, and the built frontend cannot operate against production OIDC. API location and identity cannot vary safely by deployment. + +**Improve** + +- Add a bounded streaming JSON parser, content-type enforcement, and request timeout. +- Add one top-level error boundary that logs correlation data and returns a sanitized typed 500 response. +- Implement an auth/session adapter in the frontend and use same-origin or environment-injected API configuration. +- Validate and configure CORS by environment; include credentials only when the chosen auth model requires them. +- Define a complete error-to-HTTP mapping and set JSON content type on every JSON response. +- Preserve or derive correlation/causation IDs from validated headers instead of always generating unrelated IDs. + +### P1-10 — The default backend path is not a deployable or transaction-safe runtime + +**Evidence** + +- Without `MONGODB_URI`, `server.ts:15-20` starts `MongoMemoryServer`, which is a standalone server, but every mutation uses transactions. +- `npm run build` runs `tsc --noEmit` and Vite; it creates no backend artifact. +- There is no production `start` script, and backend discovery explicitly searches for `manifest.ts` (`discover-features.ts:25`). +- Server infrastructure imports are literal `.ts` filesystem paths (`server.ts:24-40`). + +**What is wrong and why** + +The no-config development fallback starts successfully but transaction-backed mutations require a replica set and will fail. Separately, “build succeeded” means only that the frontend was bundled and TypeScript checked; there is no defined production server artifact or startup contract. + +**Improve** + +- Use `MongoMemoryReplSet` for a zero-config transactional demo, or require the documented Docker replica set and fail fast. +- Define a backend build/output strategy and `start`, `start:mcp`, and worker deployment commands. +- Make discovery extension-agnostic or bundle/generate a deterministic manifest index at build time. +- Add a smoke test that starts the built artifact and completes one real mutation. + +### P2-01 — Frontend command APIs hide failure from their callers + +**Evidence** + +- `App.run` catches errors and resolves normally (`core/frontend/App.tsx:48-62`). +- Create Task clears its input in `.then(...)` even after a failed request (`create-task/frontend/contribution.tsx:44-46`). +- `generatePlan` converts every failure to an empty list (`App.tsx:63-83`), which is indistinguishable from a valid empty plan. + +**Improve** + +Return a typed command result or rethrow after setting shared error state. Feature UI should clear or advance only on explicit success. Preserve provider/validation errors for the planning panel rather than collapsing them into `[]`. + +### P2-02 — One global model and `busy` flag couple unrelated UI behavior + +**Evidence** + +- `App.tsx:24-133` owns all feature data fetching and commands. +- `AppModel.busy` disables create, delete, status, and planning controls together. +- Filters trigger asynchronous refreshes with no cancellation or stale-response guard (`App.tsx:34-47`). + +**Improve** + +Use feature-owned query/command state keyed by operation/entity. Add abort signals or request sequencing for filter changes. Prefer a small shared query cache/event invalidation boundary over forcing all features through one shell component. + +### P2-03 — Feature discovery and registry lifecycle are fragile + +**Evidence** + +- Backend discovery recursively imports every file named `manifest.ts` without first tying it to a validated `feature.definition.json` (`discover-features.ts:19-34`). +- Frontend discovery relies on three magic export names (`contribution`, `settingsContribution`, and `contributions`) (`discover-contributions.ts:2-18`). +- The slot registry is global mutable module state with no reset/isolation API (`slot-registry.ts:12-28`). + +**Improve** + +- Discover one well-defined module per validated feature definition. +- Require one standard export, such as `frontendContributions`. +- Construct a registry instance at application bootstrap and inject it, making duplicate behavior explicit and testable. +- Decide whether duplicate names are errors; silently replacing a contribution can hide collisions. + +### P2-04 — List Tasks has no pagination and weak deterministic ordering + +**Evidence** + +- `mongo-task-capabilities.ts:18-25` returns all matching tasks. +- Sorting uses only `createdAt`, so equal timestamps have no stable tie-breaker. + +**Improve** + +Add a bounded page size and cursor based on `(createdAt, id)`, include the cursor in HTTP/MCP contracts, and create matching indexes. Keep filtering in the backend; avoid redundant client filtering once the server query is authoritative. + +### P2-05 — Configuration and health checks are too shallow + +**Evidence** + +- `config.ts` validates the idempotency lease and some production presence checks, but not `PORT`, host/origin URLs, database name, Ollama URL, or conflicting settings. +- `/health` always returns `ok` without checking readiness (`request-handler.ts:45`). +- Shutdown handles `SIGTERM` only (`server.ts:100-107`). + +**Improve** + +Parse configuration through a runtime schema, separate liveness from readiness, check MongoDB and required provider dependencies, and support `SIGINT` plus server-drain timeouts. Use structured logs with correlation IDs and avoid logging tenant identifiers unnecessarily. + +### P2-06 — Test coverage verifies the happy architecture, not its failure boundaries + +**Current strengths** + +The suite covers feature behavior, basic actor/scopes, HTTP idempotency presence, tenant-filtered Mongo operations, canonical fingerprints, route matching, transaction rollback, and simple outbox success/failure. + +**Missing high-value tests** + +- Runtime input/output schema rejection and DTO sanitization. +- Capability missing/undeclared behavior. +- Malicious tenant override attempts. +- Idempotency crash recovery and concurrent same-key calls. +- Concurrent outbox workers and expired processing leases. +- Actual event documents validated against published schemas. +- Manifest/definition/contribution parity. +- MCP schema, confirmation, auth, and retry parity with HTTP. +- Production JWT claim edge cases and frontend OIDC behavior. +- Ollama timeout, oversized response, invalid schema, retry, provenance, and explicit fallback behavior. +- React slot contract tests, feature UI tests, accessibility checks, and critical browser flows. +- Built-backend smoke test. + +### P2-07 — The frontend bundle path does excessive module work + +**Evidence** + +The production build succeeded but transformed approximately 11,706 modules and emitted a roughly 480 kB JavaScript chunk (about 149 kB gzip). It also produced thousands of MUI `"use client"` warnings. Eager feature discovery loads every contribution at startup (`discover-contributions.ts:8-11`). + +**Improve** + +- Add bundle-size budgets and bundle analysis to CI. +- Prefer lazy contribution loading by route/surface where practical. +- Verify MUI import/tree-shaking configuration and consider direct icon imports if analysis shows the barrel import is responsible. +- Configure Vite warning handling so actionable warnings are visible rather than buried. + +### P2-08 — API and domain types are duplicated and inconsistently owned + +**Evidence** + +- `core/frontend/app-model.ts` handwrites `TaskView` and `TaskSuggestion`. +- Backend features handwrite input interfaces beside generated but unused schemas. +- Task taxonomy is duplicated conceptually between TypeScript enums and multiple JSON schemas. + +**Improve** + +Generate types and validators once, expose versioned public contract packages/modules per bounded context, and add explicit domain-to-contract mappers. Domain objects may remain richer than public DTOs, but that difference must be intentional and tested. + +### P3-01 — UI semantics, accessibility, and maintainability need refinement + +**Evidence and improvements** + +- Replace `window.confirm` with the promised MUI dialog so confirmation is accessible, testable, and consistent (`delete-task/frontend/contribution.tsx:14-20`). +- Add labels/`aria-label`s to the bare `Select` controls in Create Task and AI Planning. +- Remove or wire the non-functional task-card `MoreHoriz` button (`list-tasks/frontend/contribution.tsx:62-64`). +- Replace raw priority string comparison with `TaskPriority.High` (`:71-72`). +- Break the highly compressed Settings JSX into focused components and hooks (`manage-ai-settings/frontend/contribution.tsx:20-38`). +- Move repeated visual constants into the MUI theme rather than scattering raw hex colors. +- Replace string surfaces such as `"dashboard"` and `"settings"` with a typed navigation/surface contract. + +## Recommended target architecture + +The following shape preserves the good parts of AIFA while closing the enforcement gaps: + +```text +validated feature definition + | + +-- generated/compiled input + output + MCP validators + +-- executable feature metadata checked for parity + +-- typed frontend contribution metadata + | +transport adapter (HTTP or MCP) + | + +-- authenticate -> actor + +-- validate input -> contract DTO + +-- normalize command metadata/idempotency + | +invocation runtime + | + +-- preflight exact capabilities + +-- bind actor/tenant to infrastructure capabilities + +-- execute feature outside infrastructure details + +-- validate/map output + | +unit of work + | + +-- business persistence + +-- audit envelope + +-- domain event/outbox + +-- idempotency result + +-- one atomic commit +``` + +For AI work, use a durable two-phase workflow: transactionally create/claim a planning request, perform the provider call outside a database transaction, validate/provenance-stamp the response, then atomically persist the plan, audit/event records, and completed command result. + +For the frontend, keep the shell limited to layout, navigation, authentication, error boundaries, and slot composition. Each feature should own its query/command controller. Slots should be typed through a `SlotModelMap` and receive only the contract needed for that extension point. + +## Prioritized remediation plan + +### Phase 1 — Make the claims true + +1. Add runtime input/output validation and domain-to-DTO mapping. +2. Redesign capability failure handling and exact feature capability types. +3. Bind tenancy/actor in infrastructure capabilities. +4. Make event emission conform to declared schemas. +5. Redesign idempotency and outbox atomicity/claiming. +6. Move external AI calls outside MongoDB transactions. + +### Phase 2 — Restore modularity + +1. Remove context imports and product vocabulary from core. +2. Introduce discoverable infrastructure/provider manifests. +3. Generate or parity-check executable manifests from feature definitions. +4. Replace the global `AppModel` with feature-owned controllers and typed slot contracts. +5. Generate feature identifiers instead of editing a central enum for every feature. + +### Phase 3 — Make AI and production adapters exemplary + +1. Project real schemas and safety metadata into MCP tools. +2. Implement stable MCP command IDs, confirmation, and HTTP/MCP parity tests. +3. Make provider failure/fallback/provenance explicit and schema-validated. +4. Connect workspace settings to a server-side provider router or remove the misleading controls. +5. Define a deployable backend artifact, replica-set-safe local path, production auth flow, and built-artifact smoke tests. + +### Phase 4 — Quality and scale + +1. Add the negative/concurrency/contract/frontend tests listed above. +2. Add pagination, readiness, structured telemetry, and robust shutdown. +3. Add bundle budgets, lazy feature UI loading, accessibility tests, and browser E2E coverage. + +## Verification performed during this review + +- `npm run check` — passed: feature-definition validation, generated contracts, dependency-cruiser, and TypeScript. +- `npm test` — passed: 10 tests. +- `npm run build` — passed: Vite production bundle completed, with extensive MUI module-directive warnings and one approximately 480 kB JS chunk. +- `npm ls zod --depth=1` — confirmed `zod` is currently transitive through `@modelcontextprotocol/sdk`, not a declared direct dependency. + +These green checks should be retained, but strengthened so they verify executable architecture parity and the failure boundaries identified above. diff --git a/demo/ai-todo-assistant/IMPROVEMENTS.md b/demo/ai-todo-assistant/IMPROVEMENTS.md new file mode 100644 index 0000000..e07b3f7 --- /dev/null +++ b/demo/ai-todo-assistant/IMPROVEMENTS.md @@ -0,0 +1,39 @@ +# Improvement Backlog + +This backlog captures intentional clean-code and maintainability refinements. It complements [TODO.md](TODO.md): items here are not required for the current local demonstration, but should be completed before using this project as a long-lived reference implementation. + +## Type Safety + +- [x] Type dynamic feature discovery as `Promise` rather than `Promise`. + - **Why:** Preserve compile-time guarantees from discovered manifests through routing, MCP registration, and authorization. + - **Boundary:** `core/backend/discovery` owns the typed loading and validation adapter. + +- [x] Replace remaining string-based error codes and capability declarations with enums. + - **Why:** Avoid spelling drift between features, adapters, tests, and API clients. + - **Boundary:** Add values to core shared enums; features consume them through typed contracts only. + +## HTTP Composition + +- [x] Extract route matching and JSON request parsing from `core/backend/server.ts` into typed HTTP middleware. + - **Why:** Keep the server as composition root and make route behavior independently testable. + - **Boundary:** The middleware may adapt HTTP only; it must not contain business rules or access feature infrastructure. + +- [x] Consolidate the production server and test request handler behind one HTTP composition path. + - **Why:** Prevent authorization, CORS, idempotency, and error-mapping behavior from drifting between execution paths. + - **Boundary:** `core/backend/http` owns reusable middleware; `server.ts` only wires dependencies. + +## Command Integrity + +- [x] Use a canonical request fingerprint instead of raw `JSON.stringify` for idempotency comparison. + - **Why:** Semantically identical JSON objects with a different property order must not be treated as different commands. + - **Boundary:** `core/backend/idempotency` owns canonicalization and hashing; features never calculate fingerprints. + +- [x] Introduce a transaction-runner interface and inject it into the HTTP command boundary. + - **Why:** Make transaction behavior unit-testable without MongoDB and keep the Mongo implementation replaceable. + - **Boundary:** The interface belongs in core; MongoDB supplies the infrastructure adapter. + +## Verification + +- [x] Add focused unit tests for typed discovery failures, canonical idempotency fingerprints, and transaction rollback behavior. + - **Why:** Lock in the architecture guarantees as the application gains more bounded contexts. + - **Boundary:** Tests verify core behavior through public interfaces, not database internals. diff --git a/demo/ai-todo-assistant/OPERATIONS.md b/demo/ai-todo-assistant/OPERATIONS.md new file mode 100644 index 0000000..dc6684f --- /dev/null +++ b/demo/ai-todo-assistant/OPERATIONS.md @@ -0,0 +1,24 @@ +# Operations Guide + +## Run locally + +Copy `.env.example` to `.env`, then run `docker compose up -d` and `npm run dev` from this directory. Start the local MCP server with `npm run mcp` after the API is listening. + +## Data and recovery + +MongoDB is the system of record. Back up the database with an encrypted, tested `mongodump` on a schedule appropriate to the tenant recovery objective. Restore into an isolated environment first, validate tenant counts and task-plan records, then promote following the incident runbook. + +## Monitoring + +Probe `/health` for API liveness. Alert on Mongo connection failures, outbox documents accumulating in `Pending`, repeated `VersionConflict` failures, and Ollama fallback usage. Never log task text, bearer tokens, or full model prompts in shared telemetry. + +## Security and privacy + +Production callers must provide a signed OIDC bearer token. The API verifies its issuer, audience, and signature using `OIDC_ISSUER_URL`, `OIDC_AUDIENCE`, and `OIDC_JWKS_URL`; it reads `sub`, `tenant_id`, and space-separated `scope` claims. The `x-aifa-*` headers are accepted only outside production. Set the short-lived `MCP_ACCESS_TOKEN` secret for the local stdio MCP bridge. Treat Ollama prompts and responses as tenant data; run local Ollama on trusted infrastructure and do not enable a remote model provider without a data-processing review. + +## Incident response + +1. Preserve request correlation IDs and audit events. +2. Restrict affected MCP credentials or tenant access. +3. Restore service health before replaying any outbox event. +4. Perform a tenant-scoped reconciliation from audit and task records. diff --git a/demo/ai-todo-assistant/README.md b/demo/ai-todo-assistant/README.md new file mode 100644 index 0000000..36459b5 --- /dev/null +++ b/demo/ai-todo-assistant/README.md @@ -0,0 +1,114 @@ +# Focusly — AI Todo Assistant + +Focusly is an AI-assisted task workspace built as a production-style demonstration of **AI Atomic Feature Architecture (AIFA)**. It helps people turn a natural-language goal into a practical task plan, review the suggestions, and move approved work through a focused Kanban workflow. + +![Focusly landing page](./public/readme-preview.jpg) + +## The workspace + +The workspace keeps capture, categorization, priority, and task status visible in one place. Tasks move through Todo, In Progress, and Completed while feature-owned actions remain composed through typed UI slots. + +![Focusly task workspace with sample tasks](./public/readme-workspace.jpg) + +## What the app does + +- Captures tasks across Work, Personal, Sport, Shopping, and Other categories. +- Tracks work through Todo, In Progress, and Completed states. +- Generates categorized, prioritized task suggestions from a natural-language goal. +- Keeps AI suggestions reviewable before they become real tasks. +- Supports local AI planning through Ollama and external AI clients through MCP. +- Applies the same validation, authorization, idempotency, audit, and concurrency rules to browser and AI-driven operations. + +The interface includes a public landing page and four workspace surfaces: Overview, My Tasks, AI Planner, and AI Connections. + +## How AIFA is used + +AIFA treats each product capability as a small, explicit feature contract. A feature declares what it accepts, what it returns, which capabilities it may use, where it appears in the UI, and how it is exposed over HTTP or MCP. + +```mermaid +flowchart LR + UI["React UI slots"] --> Runtime["AIFA runtime"] + HTTP["HTTP routes"] --> Runtime + MCP["MCP tools"] --> Runtime + Runtime --> Features["Discovered feature contracts"] + Features --> Caps["Granted capabilities"] + Caps --> Mongo["MongoDB"] + Caps --> AI["Ollama or AI provider"] + Caps --> Audit["Audit and outbox"] +``` + +### Atomic feature slices + +Each feature lives under `contexts//features//` and owns its complete slice: + +- `feature.definition.json` — machine-readable business, dependency, security, route, capability, MCP, and UI-slot declaration. +- `manifest.ts` — executable registration for backend, frontend, and MCP composition. +- `backend/` — feature behavior expressed against explicitly granted capabilities. +- `frontend/` — the React contribution rendered into a typed UI slot. +- `contracts/` — JSON Schema input and output contracts. +- `IMPLEMENTATION_PLAN.md` — the human-readable implementation guide. + +The current feature set includes Create Task, List Tasks, Change Task Status, Delete Task, Generate Task Plan, and Manage AI Settings. + +### Typed UI slots + +The React shell owns layout and composition, while feature slices contribute their own UI through typed slots such as: + +- `AppHeader`, `AppNavigation`, `AppContent`, and `AppFooter` +- `TaskComposer`, `TaskList`, `TaskRowActions`, and `AssistantPanel` +- `SettingsPanel` + +Frontend contributions are discovered automatically. Adding a feature does not require maintaining a central list of product components. + +### Governed runtime capabilities + +Feature code does not import databases, HTTP objects, environment variables, or provider SDKs directly. The runtime gives each feature only the capabilities declared in its definition. This keeps business logic isolated and makes unauthorized dependencies fail clearly. + +Task mutations use tenant-scoped MongoDB capabilities, optimistic version checks, idempotency keys, audit records, and an outbox. AI planning uses a provider-neutral capability, so Ollama can be replaced without changing the Generate Task Plan feature. + +### One boundary for humans and AI + +HTTP routes and MCP tools invoke the same discovered AIFA features. An AI client therefore cannot bypass the validation, authorization, confirmation, auditing, or task lifecycle rules used by the browser application. + +## Project structure + +```text +core/ + backend/ Runtime, HTTP, auth, MongoDB, MCP, audit, and outbox adapters + frontend/ React shell, app model, dynamic discovery, and typed slots + shared/ Shared enums and AIFA contracts +contexts/ + task-management/ + ai-planning/ + workspace-settings/ +tests/ Runtime, boundary, architecture, and integration coverage +``` + +See [ARCHITECTURE.md](./ARCHITECTURE.md) for the full architectural contract and [BUSINESS.md](./BUSINESS.md) for the product rationale. + +## Run locally + +Requirements: Node.js 20+, Docker, and Docker Compose. + +```bash +npm install +cp .env.example .env +docker compose up -d +npm run dev +``` + +The web app runs through Vite and the API defaults to `http://127.0.0.1:3000`. To use the local AI planner, pull the configured Ollama model if it is not already available: + +```bash +docker compose exec ollama ollama pull llama3.2 +``` + +## Quality checks + +```bash +npm run check +npm test +npm run build +``` + +These commands validate feature definitions and schemas, generate contract types, enforce dependency boundaries, type-check the project, run behavioral tests, and build the frontend. diff --git a/demo/ai-todo-assistant/TODO.md b/demo/ai-todo-assistant/TODO.md new file mode 100644 index 0000000..503fc47 --- /dev/null +++ b/demo/ai-todo-assistant/TODO.md @@ -0,0 +1,72 @@ +# AI Todo Assistant Progress + +> Architecture-review remediation is tracked separately in +> [ARCHITECTURE-REMEDIATION-TODO.md](ARCHITECTURE-REMEDIATION-TODO.md). That tracker is the +> authoritative status for findings from the 2026-08-15 architecture review. + +Update this checklist as work is completed. Read [ARCHITECTURE.md](ARCHITECTURE.md) before changing the order or scope of a task. + +## Completed + +- [x] Define the AIFA working agreement, business goal, and enterprise architecture. +- [x] Define bounded contexts for Task Management and AI Planning. +- [x] Create machine-readable feature definitions and implementation plans. +- [x] Define JSON Schema contracts, published context contracts, and event envelopes. +- [x] Define actor/tenant, MCP/OAuth, idempotency, concurrency, audit, and outbox rules. +- [x] Add architecture validation, contract type generation, dependency checks, and TypeScript checks. + +## Foundation + +- [x] Create the production project workspace structure for backend and frontend code. +- [x] Add environment configuration and `.env.example` for MongoDB, OIDC, MCP, and Ollama. +- [x] Add local development infrastructure: MongoDB replica set and Ollama service. +- [x] Add application startup, graceful shutdown, health checks, and configuration validation. +- [x] Add CI workflow for `npm run check`, tests, and build artifacts. + +## Core Platform + +- [x] Implement shared enums, result types, actor identity, correlation metadata, and generated contract exports. +- [x] Implement the AIFA runtime with declared-capability enforcement. +- [x] Implement dynamic feature-definition and manifest discovery for the backend. +- [x] Implement MongoDB connection, tenant-scoped indexed task collections, optimistic writes, and audit records. +- [x] Implement audit-backed outbox persistence and indexes; delivery adapter remains deployment-specific. +- [x] Implement an actor-resolution boundary with local development headers and production identity-adapter seam. +- [x] Implement local stdio MCP tool discovery and invocation from feature manifests. +- [x] Implement dynamic HTTP route and MCP tool registration from feature manifests. + +## Task Management Context + +- [x] Implement Task domain invariants, including optimistic versioning and valid status transitions. +- [x] Implement the Create Task feature: backend, MUI slot contribution, manifest, and tests. +- [x] Implement the List Tasks feature: backend, MUI list slot, manifest, and tests. +- [x] Implement the Change Task Status feature: backend, MUI controls, MCP tool, manifest, and tests. +- [x] Implement the Delete Task feature: backend, confirmation dialog, MCP tool, manifest, and tests. + +## AI Planning Context + +- [x] Implement persisted generated TaskPlan records and initial review state. +- [x] Implement the Ollama provider adapter with structured-output validation, timeouts, and safe local fallback. +- [x] Implement the Generate Task Plan feature: backend, MUI assistant panel, MCP tool, manifest, and tests. +- [x] Implement suggestion acceptance through the Create Task API boundary. + +## React Application + +- [x] Introduce the generic frontend query/event runtime, versioned context cache tags, and feature-local query/command hooks; replace workspace-wide command orchestration with semantic cache invalidation. +- [x] Create the MUI theme, React shell, API client, typed slot registry, and dynamic frontend discovery. +- [x] Build the category-based task board. +- [x] Build task-category and task-status filtering. +- [x] Build AI-plan review, acceptance, failure, and loading states. +- [x] Add responsive layout and empty/error states using MUI components. +- [x] Redesign the app shell with responsive header, navigation, footer, and a Kanban task board with a horizontal category rail. +- [x] Add slot-based AI connection settings for Ollama and MCP preferences. +- [x] Extract AI connection preferences into the independent Workspace Settings feature boundary. +- [x] Add a branded landing page and a modern, responsive dashboard while preserving typed slot composition. + +## Quality and Release Readiness + +- [x] Add feature-level runtime behavioral tests. +- [x] Add disposable-database MongoDB integration coverage (CI service and local opt-in script). +- [x] Add HTTP authentication/idempotency boundary and task tenant/concurrency integration coverage; MCP parity remains covered through manifest-driven adapter composition. +- [~] Browser end-to-end tests for the critical user and AI-assisted flows are intentionally deferred. +- [x] Add operational documentation for deployment, backup, monitoring, incident response, and model-provider privacy. +- [x] Review and address security, authorization, idempotency, performance, event compatibility, and data-retention requirements before release. diff --git a/demo/ai-todo-assistant/architecture-presentation.html b/demo/ai-todo-assistant/architecture-presentation.html new file mode 100644 index 0000000..33b48ce --- /dev/null +++ b/demo/ai-todo-assistant/architecture-presentation.html @@ -0,0 +1,1233 @@ + + + + + + + AIFA — Architecture for Easy Extension + + + + +
      Use ↑ ↓ or space
      + + +
      +
      +
      +
      +

      AI Atomic Feature Architecture

      +

      Extend without unraveling.

      +

      AIFA turns every capability into a small, explicit feature contract—composed through governed backend capabilities and typed frontend UI slots.

      +
      + Feature modules + Capability boundaries + UI slots + AI-friendly contracts +
      +
      +
      +
      +
      +
      F
      +
      +
      + 01 / 09 +
      + +
      +
      +

      The core idea

      +
      A feature carries everything needed to understand and extend one behavior—without learning the whole application first.
      +
      +
      01 · CONTRACT

      Inputs, outputs, failures, capabilities, routes, tools, events, and slots are explicit.

      +
      02 · ISOLATION

      Business logic stays inside its bounded context and does not import infrastructure.

      +
      03 · DISCOVERY

      Features register locally; generic composition discovers them rather than maintaining product lists.

      +
      04 · GOVERNANCE

      Every HTTP, MCP, and UI path reaches the same governed feature boundary.

      +
      +
      + 02 / 09 +
      + +
      +
      +

      One feature · two composition paths

      +

      Backend and frontend extend independently—through the same feature boundary.

      +
      +
      + + + +
      +
      +
      +
      Backend extension path
      +
      +
      HTTP / MCPTransport adapts requests into one contract
      +
      +
      AIFA runtimeValidates actor, metadata, and granted capabilities
      +
      +
      CapabilitiesTenant-bound persistence, AI, clock, audit, events
      +
      +
      +
      Feature contract
      +
      +
      Frontend extension path
      +
      +
      Feature UIOwn view, interaction, and local state
      +
      +
      Slot registryDiscovers, orders, and types contributions
      +
      +
      App shellLayout and composition—not product behavior
      +
      +
      +
      +

      The feature is the stable center. Transport, infrastructure, and shell layout remain replaceable adapters around it.

      +
      +
      + 03 / 09 +
      + +
      +
      +

      Backend extension

      +

      Add behavior locally. Let the platform compose it.

      +

      A new backend feature declares what it needs; the runtime supplies only those capabilities. Infrastructure never leaks into feature logic.

      +
      +
      + contexts/task-management/
      + └── features/archive-task/
      +     ├── feature.definition.json
      +     ├── manifest.ts
      +     ├── contracts/
      +     │   ├── input.schema.json
      +     │   └── output.schema.json
      +     ├── backend/
      +     │   └── feature.ts
      +     └── frontend/
      +         └── contribution.tsx
      + # one folder = one change surface +
      +
      +
      1
      Declare the contract

      Input, output, failures, route, MCP tool, scopes, idempotency, and events.

      Feature
      +
      2
      Implement pure orchestration

      Use actor, metadata, and named capabilities—never MongoDB, HTTP, or provider SDKs.

      Feature
      +
      3
      Discover and validate

      Composition finds the manifest, validates contracts, routes, dependencies, and duplicates.

      Core
      +
      4
      Bind governed capabilities

      The runtime supplies tenant-safe adapters for storage, audit, events, AI, IDs, and clocks.

      Runtime
      +
      5
      Expose everywhere consistently

      HTTP and MCP invoke the same feature, validation, authorization, and audit path.

      Adapters
      +
      +
      +
      + 04 / 09 +
      + +
      +
      +

      Frontend extension with UI slots

      +

      The shell owns the where. Features own the what.

      +
      +
      +
      1

      The shell publishes named slots

      Examples: AppHeader, AppContent, AssistantPanel, TaskComposer, and TaskRowActions.

      +
      2

      Each slot has a small typed model

      A contribution receives only the data and commands that extension point promises.

      +
      3

      Features register contributions locally

      Discovery loads them deterministically; ordering is metadata, not a central import list.

      +
      4

      Composition stays open

      Add, replace, reorder, or remove feature UI without rewriting the application shell.

      +
      +
      +
      AppHeader slot
      +
      AssistantPanel slot
      +
      AppContent slot
      +
      RowActions slot
      +
      +
      +
      + 05 / 09 +
      + +
      +
      +

      Why the feature contract matters

      +

      One source of intent serves humans, runtime, and AI agents.

      +
      +
      + For developers +

      Local reasoning

      +

      The folder explains what the feature does, what it consumes, what it emits, and where it appears. Less archaeology, fewer hidden conventions.

      +
      +
      + For the platform +

      Executable governance

      +

      Validation, authorization, tenancy, idempotency, capability access, auditing, and events can be enforced consistently at one boundary.

      +
      +
      + For AI agents +

      Bounded context windows

      +

      An agent can plan, implement, test, or review one feature using explicit machine-readable contracts—without guessing the rest of the system.

      +
      +
      +
      + 06 / 09 +
      + +
      +
      +

      Benefits

      +

      Modularity that improves delivery—not just folder structure.

      +
      +
      01

      Easy extension

      Add a feature slice and contribution; generic discovery handles composition.

      +
      02

      Low coupling

      Features depend on contracts and capabilities, not concrete infrastructure or peers.

      +
      03

      Independent testing

      Run behavior with small capability fakes, then contract-test adapters separately.

      +
      04

      Replaceable adapters

      Swap MongoDB, AI providers, auth, HTTP, or MCP without rewriting feature logic.

      +
      05

      Consistent channels

      Browser and AI clients use the same validation, permissions, business rules, and audit trail.

      +
      06

      Parallel delivery

      Teams and agents work in separate feature folders with fewer shared-file conflicts.

      +
      07

      Safer AI changes

      Explicit scope, dependencies, contracts, and acceptance criteria constrain automated work.

      +
      08

      Evolution by addition

      New capabilities and UI appear through extension points instead of repeated core rewrites.

      +
      +
      + 07 / 09 +
      + +
      +
      +

      The practical difference

      +

      From coordinated edits to a governed extension.

      +
      +
      +

      Traditional feature addition

      +
        +
      • Edit route registries, services, repositories, screens, and shared state.
      • +
      • Copy security and error handling into another execution path.
      • +
      • Understand transitive dependencies before making a safe change.
      • +
      • Coordinate shared files and resolve recurring merge conflicts.
      • +
      +
      +
      VS
      +
      +

      AIFA feature addition

      +
        +
      • Add one local slice containing contract, behavior, UI, and tests.
      • +
      • Declare capabilities, scopes, events, routes, tools, and slots explicitly.
      • +
      • Let discovery and architecture validation compose the feature.
      • +
      • Reuse the same governed operation across browser and AI clients.
      • +
      +
      +
      +
      + 08 / 09 +
      + +
      +
      +

      The condition for success

      +

      AIFA works when the boundaries are executable—not merely documented.

      +

      The architecture remains easy to extend because automated guardrails keep every new feature honest.

      +
      +

      Contract validation

      Validate every input, output, event, and MCP tool shape at runtime.

      +

      Dependency rules

      Reject core-to-product, feature-to-feature, and cross-context implementation imports.

      +

      Typed slots

      Bind every slot name to a precise model and fail contribution drift in CI.

      +

      Bound capabilities

      Grant only declared operations and bind actor, tenant, transaction, and audit centrally.

      +

      Parity tests

      Prove HTTP, MCP, and UI adapters reach the same feature behavior and safety checks.

      +
      +
      Small features · explicit contracts · safe composition
      +
      + 09 / 09 +
      +
      + + + + diff --git a/demo/ai-todo-assistant/composition/generated/frontend-modules.ts b/demo/ai-todo-assistant/composition/generated/frontend-modules.ts new file mode 100644 index 0000000..1367630 --- /dev/null +++ b/demo/ai-todo-assistant/composition/generated/frontend-modules.ts @@ -0,0 +1,39 @@ +/* Generated from feature definitions. Do not edit manually. */ +export const frontendModules = [ + { + id: "generate-task-plan", + slots: ["AssistantPanel"] as const, + eventConsumers: [] as const, + load: () => import("../../contexts/ai-planning/features/generate-task-plan/frontend/contribution.js"), + }, + { + id: "change-task-status", + slots: ["TaskRowActions"] as const, + eventConsumers: [{"name":"invalidate-task-collection-after-status-change","eventType":"TaskStatusChangedV1","contract":"task-management/contracts/events/v1/task-status-changed.schema.json"}] as const, + load: () => import("../../contexts/task-management/features/change-task-status/frontend/contribution.js"), + }, + { + id: "create-task", + slots: ["TaskComposer"] as const, + eventConsumers: [{"name":"invalidate-task-collection-after-create","eventType":"TaskCreatedV1","contract":"task-management/contracts/events/v1/task-created.schema.json"}] as const, + load: () => import("../../contexts/task-management/features/create-task/frontend/contribution.js"), + }, + { + id: "delete-task", + slots: ["TaskRowActions"] as const, + eventConsumers: [{"name":"invalidate-task-collection-after-delete","eventType":"TaskDeletedV1","contract":"task-management/contracts/events/v1/task-deleted.schema.json"}] as const, + load: () => import("../../contexts/task-management/features/delete-task/frontend/contribution.js"), + }, + { + id: "list-tasks", + slots: ["TaskList","AppHeader","AppNavigation","AppContent","AppFooter"] as const, + eventConsumers: [] as const, + load: () => import("../../contexts/task-management/features/list-tasks/frontend/contribution.js"), + }, + { + id: "manage-ai-settings", + slots: ["AppNavigation","AppContent","SettingsPanel"] as const, + eventConsumers: [] as const, + load: () => import("../../contexts/workspace-settings/features/manage-ai-settings/frontend/contribution.js"), + }, +] as const; diff --git a/demo/ai-todo-assistant/contexts/ai-planning/contracts/events/v1/task-plan-generated.schema.json b/demo/ai-todo-assistant/contexts/ai-planning/contracts/events/v1/task-plan-generated.schema.json index 6884559..d7177e8 100644 --- a/demo/ai-todo-assistant/contexts/ai-planning/contracts/events/v1/task-plan-generated.schema.json +++ b/demo/ai-todo-assistant/contexts/ai-planning/contracts/events/v1/task-plan-generated.schema.json @@ -1 +1,30 @@ -{"$schema":"https://json-schema.org/draft/2020-12/schema","title":"TaskPlanGeneratedV1","type":"object","additionalProperties":false,"required":["eventId","eventType","schemaVersion","tenantId","actorId","correlationId","causationId","idempotencyKey","planId","occurredAt"],"properties":{"eventId":{"type":"string"},"eventType":{"const":"TaskPlanGeneratedV1"},"schemaVersion":{"const":1},"tenantId":{"type":"string"},"actorId":{"type":"string"},"correlationId":{"type":"string"},"causationId":{"type":"string"},"idempotencyKey":{"type":"string"},"planId":{"type":"string"},"occurredAt":{"type":"string","format":"date-time"}}} +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "title": "TaskPlanGeneratedV1", + "type": "object", + "additionalProperties": false, + "required": [ + "eventId", + "eventType", + "schemaVersion", + "tenantId", + "actorId", + "correlationId", + "causationId", + "idempotencyKey", + "planId", + "occurredAt" + ], + "properties": { + "eventId": { "type": "string" }, + "eventType": { "const": "TaskPlanGeneratedV1" }, + "schemaVersion": { "const": 1 }, + "tenantId": { "type": "string" }, + "actorId": { "type": "string" }, + "correlationId": { "type": "string" }, + "causationId": { "type": "string" }, + "idempotencyKey": { "type": "string" }, + "planId": { "type": "string" }, + "occurredAt": { "type": "string", "format": "date-time" } + } +} diff --git a/demo/ai-todo-assistant/contexts/ai-planning/domain/task-plan.ts b/demo/ai-todo-assistant/contexts/ai-planning/domain/task-plan.ts new file mode 100644 index 0000000..0ceb03b --- /dev/null +++ b/demo/ai-todo-assistant/contexts/ai-planning/domain/task-plan.ts @@ -0,0 +1,17 @@ +import type { TaskPlanSuggestion } from "./task-planning.js"; +export enum TaskPlanStatus { + Generated = "Generated", + Accepted = "Accepted", + Archived = "Archived", +} +export interface TaskPlan { + id: string; + tenantId: string; + goal: string; + suggestions: TaskPlanSuggestion[]; + status: TaskPlanStatus; + createdAt: string; + provider: string; + model: string; + promptVersion: string; +} diff --git a/demo/ai-todo-assistant/contexts/ai-planning/domain/task-planning.ts b/demo/ai-todo-assistant/contexts/ai-planning/domain/task-planning.ts new file mode 100644 index 0000000..47eb7d1 --- /dev/null +++ b/demo/ai-todo-assistant/contexts/ai-planning/domain/task-planning.ts @@ -0,0 +1,34 @@ +import { + TaskCategory, + TaskPriority, +} from "../../task-management/contracts/v1/task-taxonomy.js"; +import type { TaskPlan } from "./task-plan.js"; +import type { PlatformCapabilities } from "../../../core/shared/aifa.js"; + +export interface TaskPlanSuggestion { + title: string; + category: TaskCategory; + priority: TaskPriority; + rationale: string; +} +export interface GenerateTaskPlanRequest { + goal: string; + category: TaskCategory; + priority: TaskPriority; +} +export type TaskPlanProviderResult = + | { + ok: true; + suggestions: TaskPlanSuggestion[]; + provenance: { provider: string; model: string; promptVersion: string; latencyMs: number }; + } + | { + ok: false; + code: "ProviderUnavailable" | "AssistantResponseInvalid"; + message: string; + }; +export interface AiPlanningPersistenceCapabilities { + AssistantGenerateTaskPlan(input: GenerateTaskPlanRequest): Promise; + TaskPlanCreate(input: { plan: Omit }): Promise; +} +export type AiPlanningCapabilities = AiPlanningPersistenceCapabilities & PlatformCapabilities; diff --git a/demo/ai-todo-assistant/contexts/ai-planning/features/generate-task-plan/backend/feature.ts b/demo/ai-todo-assistant/contexts/ai-planning/features/generate-task-plan/backend/feature.ts new file mode 100644 index 0000000..733b82b --- /dev/null +++ b/demo/ai-todo-assistant/contexts/ai-planning/features/generate-task-plan/backend/feature.ts @@ -0,0 +1,70 @@ +import { + CapabilityName, + DomainEventType, + ErrorCode, +} from "../../../../../core/shared/architecture-enums.js"; +import { FeatureName } from "../../../../../core/shared/generated/feature-names.js"; +import type { AifaFeature } from "../../../../../core/shared/aifa.js"; +import type { + AiPlanningCapabilities, + GenerateTaskPlanRequest, + TaskPlanSuggestion, +} from "../../../domain/task-planning.js"; +import { TaskPlanStatus } from "../../../domain/task-plan.js"; +export type GenerateTaskPlanInput = GenerateTaskPlanRequest; +type GenerateTaskPlanCapabilities = Pick< + AiPlanningCapabilities, + "AssistantGenerateTaskPlan" | "TaskPlanCreate" | "IdCreate" | "ClockNow" | "DomainEventEmit" +>; +export const feature: AifaFeature< + GenerateTaskPlanInput, + { + suggestions: TaskPlanSuggestion[]; + provenance: { provider: string; model: string; promptVersion: string; latencyMs: number }; + }, + GenerateTaskPlanCapabilities +> = { + name: FeatureName.GenerateTaskPlan, + capabilities: [ + CapabilityName.AssistantGenerateTaskPlan, + CapabilityName.TaskPlanCreate, + CapabilityName.IdCreate, + CapabilityName.ClockNow, + CapabilityName.DomainEventEmit, + ], + async execute(context) { + const goal = context.input.goal.trim(); + if (!goal) return context.fail(ErrorCode.InvalidInput, "A goal is required"); + const generated = await context.capabilities.AssistantGenerateTaskPlan({ + ...context.input, + goal, + }); + if (!generated.ok) + return context.fail( + generated.code === "AssistantResponseInvalid" + ? ErrorCode.AssistantResponseInvalid + : ErrorCode.ProviderUnavailable, + generated.message, + ); + const plan = await context.capabilities.TaskPlanCreate({ + plan: { + id: await context.capabilities.IdCreate(), + goal, + suggestions: generated.suggestions, + status: TaskPlanStatus.Generated, + createdAt: await context.capabilities.ClockNow(), + provider: generated.provenance.provider, + model: generated.provenance.model, + promptVersion: generated.provenance.promptVersion, + }, + }); + await context.capabilities.DomainEventEmit({ + eventType: DomainEventType.TaskPlanGeneratedV1, + data: { planId: plan.id }, + }); + return context.ok({ + suggestions: generated.suggestions, + provenance: generated.provenance, + }); + }, +}; diff --git a/demo/ai-todo-assistant/contexts/ai-planning/features/generate-task-plan/contracts/input.schema.json b/demo/ai-todo-assistant/contexts/ai-planning/features/generate-task-plan/contracts/input.schema.json index 998ffb0..474d12e 100644 --- a/demo/ai-todo-assistant/contexts/ai-planning/features/generate-task-plan/contracts/input.schema.json +++ b/demo/ai-todo-assistant/contexts/ai-planning/features/generate-task-plan/contracts/input.schema.json @@ -1 +1,12 @@ -{"$schema":"https://json-schema.org/draft/2020-12/schema","type":"object","additionalProperties":false,"required":["goal","category","priority","idempotencyKey"],"properties":{"goal":{"type":"string","minLength":1,"maxLength":4000},"category":{"enum":["Work","Personal","Sport","Shopping","Other"]},"priority":{"enum":["Low","Medium","High"]},"idempotencyKey":{"type":"string","minLength":16,"maxLength":128}}} +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "title": "GenerateTaskPlanInputV1", + "type": "object", + "additionalProperties": false, + "required": ["goal", "category", "priority"], + "properties": { + "goal": { "type": "string", "minLength": 1, "maxLength": 4000 }, + "category": { "enum": ["Work", "Personal", "Sport", "Shopping", "Other"] }, + "priority": { "enum": ["Low", "Medium", "High"] } + } +} diff --git a/demo/ai-todo-assistant/contexts/ai-planning/features/generate-task-plan/contracts/output.schema.json b/demo/ai-todo-assistant/contexts/ai-planning/features/generate-task-plan/contracts/output.schema.json index 6b3385d..0590ed5 100644 --- a/demo/ai-todo-assistant/contexts/ai-planning/features/generate-task-plan/contracts/output.schema.json +++ b/demo/ai-todo-assistant/contexts/ai-planning/features/generate-task-plan/contracts/output.schema.json @@ -1 +1,26 @@ -{"$schema":"https://json-schema.org/draft/2020-12/schema","type":"object","additionalProperties":false,"required":["suggestions"],"properties":{"suggestions":{"type":"array","items":{"$ref":"https://aifa.dev/ai-planning/contracts/v1/task-plan-suggestion.schema.json"}}}} +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "title": "GenerateTaskPlanOutputV1", + "type": "object", + "additionalProperties": false, + "required": ["suggestions", "provenance"], + "properties": { + "suggestions": { + "type": "array", + "items": { + "$ref": "https://aifa.dev/ai-planning/contracts/v1/task-plan-suggestion.schema.json" + } + }, + "provenance": { + "type": "object", + "additionalProperties": false, + "required": ["provider", "model", "promptVersion", "latencyMs"], + "properties": { + "provider": { "type": "string" }, + "model": { "type": "string" }, + "promptVersion": { "type": "string" }, + "latencyMs": { "type": "number", "minimum": 0 } + } + } + } +} diff --git a/demo/ai-todo-assistant/contexts/ai-planning/features/generate-task-plan/feature.definition.json b/demo/ai-todo-assistant/contexts/ai-planning/features/generate-task-plan/feature.definition.json index 8ff9f24..a314454 100644 --- a/demo/ai-todo-assistant/contexts/ai-planning/features/generate-task-plan/feature.definition.json +++ b/demo/ai-todo-assistant/contexts/ai-planning/features/generate-task-plan/feature.definition.json @@ -4,15 +4,52 @@ "boundedContext": "ai-planning", "businessNeed": "Save users from manually decomposing goals by generating reviewable, categorized task suggestions from natural language.", "dependsOn": [], - "consumesContracts": ["task-management/contracts/v1/task-taxonomy.schema.json"], - "backend": { "route": "/api/assistant/task-plan", "method": "POST", "capabilities": ["AssistantGenerateTaskPlan", "AuditRecord"], "mcp": { "exposed": true, "toolName": "generate_task_plan" } }, + "consumesContracts": [ + "task-management/contracts/v1/task-taxonomy.schema.json", + "task-management/contracts/v1/task-cache.schema.json" + ], + "backend": { + "route": "/api/assistant/task-plan", + "method": "POST", + "capabilities": [ + "AssistantGenerateTaskPlan", + "TaskPlanCreate", + "IdCreate", + "ClockNow", + "DomainEventEmit" + ], + "mcp": { "exposed": true, "toolName": "generate_task_plan" } + }, "frontend": { "slots": ["AssistantPanel"] }, - "contracts": { "input": "contracts/input.schema.json", "output": "contracts/output.schema.json", "mcpInput": "contracts/input.schema.json" }, + "contracts": { + "input": "contracts/input.schema.json", + "output": "contracts/output.schema.json", + "mcpInput": "contracts/input.schema.json" + }, "events": ["ai-planning/contracts/events/v1/task-plan-generated.schema.json"], - "security": { "mutation": true, "actorRequired": true, "idempotent": true, "requiresConfirmation": false, "concurrency": "none", "mcpScopes": ["AssistantPlanGenerate"] }, + "security": { + "mutation": true, + "actorRequired": true, + "idempotent": true, + "requiresConfirmation": false, + "concurrency": "none", + "mcpScopes": ["AssistantPlanGenerate"] + }, "workItems": [ - { "id": "AIP-1", "description": "Define a provider-neutral AI planning capability and Ollama adapter.", "acceptanceCriterion": "The feature has no direct dependency on an AI SDK." }, - { "id": "AIP-2", "description": "Validate AI suggestions into TaskCategory and TaskPriority enums.", "acceptanceCriterion": "Invalid provider output returns AssistantResponseInvalid." }, - { "id": "AIP-3", "description": "Register the MUI suggestion-review panel.", "acceptanceCriterion": "Accepted suggestions use the create-task API boundary." } + { + "id": "AIP-1", + "description": "Define a provider-neutral AI planning capability and Ollama adapter.", + "acceptanceCriterion": "The feature has no direct dependency on an AI SDK." + }, + { + "id": "AIP-2", + "description": "Validate AI suggestions into TaskCategory and TaskPriority enums.", + "acceptanceCriterion": "Invalid provider output returns AssistantResponseInvalid." + }, + { + "id": "AIP-3", + "description": "Register the MUI suggestion-review panel.", + "acceptanceCriterion": "Accepted suggestions use the create-task API boundary." + } ] } diff --git a/demo/ai-todo-assistant/contexts/ai-planning/features/generate-task-plan/frontend/contribution.tsx b/demo/ai-todo-assistant/contexts/ai-planning/features/generate-task-plan/frontend/contribution.tsx new file mode 100644 index 0000000..bc17206 --- /dev/null +++ b/demo/ai-todo-assistant/contexts/ai-planning/features/generate-task-plan/frontend/contribution.tsx @@ -0,0 +1,217 @@ +import ArrowForwardRounded from "@mui/icons-material/ArrowForwardRounded"; +import AutoAwesomeRounded from "@mui/icons-material/AutoAwesomeRounded"; +import BoltRounded from "@mui/icons-material/BoltRounded"; +import { + Alert, + Box, + Button, + Chip, + MenuItem, + Paper, + Select, + Stack, + TextField, + Typography, +} from "@mui/material"; +import { useState } from "react"; +import { + TaskCategory, + TaskPriority, +} from "../../../../task-management/contracts/v1/task-taxonomy.js"; +import type { TaskPlanSuggestionV1 } from "../../../../../core/shared/generated/contracts.js"; +import type { SlotContribution } from "../../../../../core/frontend/slots/slot-registry.js"; +import { SlotName } from "../../../../../core/shared/architecture-enums.js"; +import { useAcceptTaskSuggestion, useGenerateTaskPlan } from "./use-generate-task-plan.js"; + +function Planner() { + const [goal, setGoal] = useState(""); + const [category, setCategory] = useState(TaskCategory.Work); + const [priority, setPriority] = useState(TaskPriority.Medium); + const [suggestions, setSuggestions] = useState([]); + const generatePlan = useGenerateTaskPlan(); + const acceptSuggestion = useAcceptTaskSuggestion(); + return ( + + {(generatePlan.error || acceptSuggestion.error) && ( + + {(generatePlan.error ?? acceptSuggestion.error)?.message} + + )} + + + + + + + + + AI COPILOT + + + + Turn a goal into a clear next move. + + + Describe the outcome. Your assistant will shape the steps. + + + + setGoal(event.target.value)} + inputProps={{ "aria-label": "Goal for AI planning" }} + sx={{ "& .MuiOutlinedInput-root": { bgcolor: "white", borderRadius: 2 } }} + /> + + + + + + + + + {suggestions.length > 0 && ( + + {suggestions.map((suggestion) => ( + + + + + {suggestion.title} + + {suggestion.rationale} + + + + + + + ))} + + )} + + ); +} + +export const contribution: SlotContribution = { + slot: SlotName.AssistantPanel, + name: "task-plan-review", + order: 10, + render: () => , +}; diff --git a/demo/ai-todo-assistant/contexts/ai-planning/features/generate-task-plan/frontend/use-generate-task-plan.ts b/demo/ai-todo-assistant/contexts/ai-planning/features/generate-task-plan/frontend/use-generate-task-plan.ts new file mode 100644 index 0000000..1f8964f --- /dev/null +++ b/demo/ai-todo-assistant/contexts/ai-planning/features/generate-task-plan/frontend/use-generate-task-plan.ts @@ -0,0 +1,45 @@ +import { api } from "../../../../../core/frontend/api.js"; +import { useMutation, useQueryClient } from "../../../../../core/frontend/query/react-query.js"; +import type { + TaskPlanSuggestionV1, + TaskViewV1, +} from "../../../../../core/shared/generated/contracts.js"; +import { TaskCacheTag } from "../../../../task-management/contracts/v1/task-cache.js"; +import type { + TaskCategory, + TaskPriority, +} from "../../../../task-management/contracts/v1/task-taxonomy.js"; + +interface GenerateTaskPlanInput { + goal: string; + category: TaskCategory; + priority: TaskPriority; +} + +export function useGenerateTaskPlan() { + return useMutation({ + mutationFn: async (input) => + ( + await api<{ suggestions: TaskPlanSuggestionV1[] }>("/api/assistant/task-plan", { + method: "POST", + headers: { "idempotency-key": crypto.randomUUID() }, + body: JSON.stringify(input), + }) + ).suggestions, + }); +} + +export function useAcceptTaskSuggestion() { + const queryClient = useQueryClient(); + return useMutation({ + mutationFn: async ({ title, category, priority }) => + ( + await api<{ task: TaskViewV1 }>("/api/tasks", { + method: "POST", + headers: { "idempotency-key": crypto.randomUUID() }, + body: JSON.stringify({ title, category, priority }), + }) + ).task, + onSuccess: () => queryClient.invalidateTags([TaskCacheTag.Collection]), + }); +} diff --git a/demo/ai-todo-assistant/contexts/ai-planning/features/generate-task-plan/manifest.ts b/demo/ai-todo-assistant/contexts/ai-planning/features/generate-task-plan/manifest.ts new file mode 100644 index 0000000..7bb0cd6 --- /dev/null +++ b/demo/ai-todo-assistant/contexts/ai-planning/features/generate-task-plan/manifest.ts @@ -0,0 +1,20 @@ +import { + HttpMethod, + PermissionScope, + SlotName, +} from "../../../../core/shared/architecture-enums.js"; +import { feature } from "./backend/feature.js"; +import type { FeatureManifest } from "../../../../core/shared/feature-manifest.js"; +export const manifest = { + name: feature.name, + backend: { + method: HttpMethod.Post, + route: "/api/assistant/task-plan", + feature, + requiredScopes: [PermissionScope.AssistantPlanGenerate], + }, + frontend: { + contributions: [{ slot: SlotName.AssistantPanel, name: "task-plan-review" }], + }, + mcp: { toolName: "generate_task_plan" }, +} satisfies FeatureManifest; diff --git a/demo/ai-todo-assistant/contexts/ai-planning/infrastructure/manifest.ts b/demo/ai-todo-assistant/contexts/ai-planning/infrastructure/manifest.ts new file mode 100644 index 0000000..3c5feef --- /dev/null +++ b/demo/ai-todo-assistant/contexts/ai-planning/infrastructure/manifest.ts @@ -0,0 +1,28 @@ +import type { ContextInfrastructureManifest } from "../../../core/backend/discovery/context-infrastructure.js"; +import { CapabilityName } from "../../../core/shared/architecture-enums.js"; +import { createMongoTaskPlanCapabilities, ensureTaskPlanIndexes } from "./mongo-task-plan-capabilities.js"; +import { createOllamaTaskPlanner } from "./ollama-task-planner.js"; +const planners = new Map>(); + +export const infrastructureManifest = { + name: "ai-planning", + externalCapabilities: [CapabilityName.AssistantGenerateTaskPlan], + health() { + return { providers: [...planners.values()].map((planner) => planner.health()) }; + }, + ensureIndexes: ensureTaskPlanIndexes, + createCapabilities({ database, actor, session, settings }) { + const baseUrl = settings.ollamaBaseUrl ?? "http://127.0.0.1:11434"; + const model = settings.ollamaModel ?? "llama3.2"; + const key = `${baseUrl}\n${model}`; + let planner = planners.get(key); + if (!planner) { + planner = createOllamaTaskPlanner(baseUrl, model); + planners.set(key, planner); + } + return { + ...createMongoTaskPlanCapabilities(database, actor, session), + AssistantGenerateTaskPlan: planner, + }; + }, +} satisfies ContextInfrastructureManifest; diff --git a/demo/ai-todo-assistant/contexts/ai-planning/infrastructure/mongo-task-plan-capabilities.ts b/demo/ai-todo-assistant/contexts/ai-planning/infrastructure/mongo-task-plan-capabilities.ts new file mode 100644 index 0000000..a12865e --- /dev/null +++ b/demo/ai-todo-assistant/contexts/ai-planning/infrastructure/mongo-task-plan-capabilities.ts @@ -0,0 +1,22 @@ +import type { ClientSession, Db } from "mongodb"; +import type { TaskPlan } from "../domain/task-plan.js"; +import type { Actor } from "../../../core/shared/aifa.js"; +export function createMongoTaskPlanCapabilities( + database: Db, + actor: Actor, + session?: ClientSession, +) { + const plans = database.collection("task_plans"); + return { + async TaskPlanCreate({ plan }: { plan: Omit }) { + const tenantPlan = { ...plan, tenantId: actor.tenantId }; + await plans.insertOne(tenantPlan, { session }); + return tenantPlan; + }, + }; +} +export async function ensureTaskPlanIndexes(database: Db): Promise { + await database + .collection("task_plans") + .createIndex({ tenantId: 1, createdAt: -1 }, { name: "tenant_plan_time" }); +} diff --git a/demo/ai-todo-assistant/contexts/ai-planning/infrastructure/ollama-task-planner.ts b/demo/ai-todo-assistant/contexts/ai-planning/infrastructure/ollama-task-planner.ts new file mode 100644 index 0000000..53b0e39 --- /dev/null +++ b/demo/ai-todo-assistant/contexts/ai-planning/infrastructure/ollama-task-planner.ts @@ -0,0 +1,146 @@ +import { readFile } from "node:fs/promises"; +import { createRequire } from "node:module"; +import type { + GenerateTaskPlanRequest, + TaskPlanProviderResult, + TaskPlanSuggestion, +} from "../domain/task-planning.js"; +import { + TaskCategory, + TaskPriority, +} from "../../task-management/contracts/v1/task-taxonomy.js"; +const require = createRequire(import.meta.url); +const Ajv2020 = require("ajv/dist/2020").default as typeof import("ajv/dist/2020.js").Ajv2020; + +interface OllamaResponse { + response?: string; +} + +const promptVersion = "task-plan-v1"; +const maximumResponseBytes = 262_144; +const suggestionSchema = JSON.parse( + await readFile(new URL("../contracts/v1/task-plan-suggestion.schema.json", import.meta.url), "utf8"), +) as Record; +const { $schema: _schema, $id: _id, ...suggestionShape } = suggestionSchema; +const suggestionsSchema = { + type: "array", + minItems: 1, + maxItems: 10, + items: suggestionShape, +} as const; +const validateSuggestions = new Ajv2020({ allErrors: true, strict: false }).compile( + suggestionsSchema, +); + +export interface ProviderHealth { + requests: number; + successes: number; + failures: number; + rejected: number; + active: number; + circuitOpenUntil?: string; + lastLatencyMs?: number; +} +export type OllamaTaskPlanner = (( + input: GenerateTaskPlanRequest, +) => Promise) & { health(): ProviderHealth }; + +export function createOllamaTaskPlanner( + baseUrl: string, + model: string, + dependencies: { fetch?: typeof fetch; now?: () => number } = {}, +): OllamaTaskPlanner { + const request = dependencies.fetch ?? fetch; + const now = dependencies.now ?? Date.now; + const health: ProviderHealth = { requests: 0, successes: 0, failures: 0, rejected: 0, active: 0 }; + const recentRequests: number[] = []; + let consecutiveFailures = 0; + let circuitOpenUntil = 0; + const planner = async (input: GenerateTaskPlanRequest): Promise => { + const currentTime = now(); + while (recentRequests[0] !== undefined && recentRequests[0] < currentTime - 60_000) + recentRequests.shift(); + if (circuitOpenUntil > currentTime || health.active >= 4 || recentRequests.length >= 30) { + health.rejected++; + return { + ok: false, + code: "ProviderUnavailable", + message: circuitOpenUntil > currentTime ? "AI provider circuit is open" : "AI provider is rate limited", + }; + } + recentRequests.push(currentTime); + health.requests++; + health.active++; + try { + const startedAt = performance.now(); + const prompt = `Create 3 concise, reviewable task suggestions for this goal: ${input.goal}. Use category values ${Object.values(TaskCategory).join(", ")} and priority values ${Object.values(TaskPriority).join(", ")}. Default to category=${input.category} and priority=${input.priority}.`; + let lastFailure = "AI provider is unavailable"; + for (let attempt = 0; attempt < 2; attempt++) { + try { + const response = await request(`${baseUrl}/api/generate`, { + method: "POST", + headers: { "content-type": "application/json" }, + body: JSON.stringify({ + model, + prompt, + stream: false, + format: suggestionsSchema, + options: { temperature: 0.2 }, + }), + signal: AbortSignal.timeout(15_000), + }); + if (!response.ok) { + lastFailure = `AI provider returned HTTP ${response.status}`; + if (response.status < 500 && response.status !== 429) break; + continue; + } + const raw = await response.text(); + if (Buffer.byteLength(raw) > maximumResponseBytes) + return failure("AssistantResponseInvalid", "AI response was too large"); + const payload = JSON.parse(raw) as OllamaResponse; + const parsed: unknown = JSON.parse(payload.response ?? "null"); + if (!validateSuggestions(parsed)) + return failure( + "AssistantResponseInvalid", + "AI response did not satisfy the task-plan contract", + ); + consecutiveFailures = 0; + circuitOpenUntil = 0; + health.successes++; + health.lastLatencyMs = Math.round(performance.now() - startedAt); + return { + ok: true, + suggestions: parsed as TaskPlanSuggestion[], + provenance: { + provider: "ollama", + model, + promptVersion, + latencyMs: health.lastLatencyMs, + }, + }; + } catch (cause) { + lastFailure = cause instanceof Error ? cause.message : lastFailure; + } + } + return failure("ProviderUnavailable", lastFailure); + function failure( + code: "ProviderUnavailable" | "AssistantResponseInvalid", + message: string, + ): TaskPlanProviderResult { + health.failures++; + health.lastLatencyMs = Math.round(performance.now() - startedAt); + consecutiveFailures++; + if (consecutiveFailures >= 3) { + circuitOpenUntil = now() + 30_000; + health.circuitOpenUntil = new Date(circuitOpenUntil).toISOString(); + } + return { ok: false, code, message }; + } + } finally { + health.active--; + } + }; + const instrumented = planner as OllamaTaskPlanner; + instrumented.health = () => ({ ...health }); + return instrumented; +} diff --git a/demo/ai-todo-assistant/contexts/task-management/contracts/events/v1/task-created.schema.json b/demo/ai-todo-assistant/contexts/task-management/contracts/events/v1/task-created.schema.json index a9c0725..3730898 100644 --- a/demo/ai-todo-assistant/contexts/task-management/contracts/events/v1/task-created.schema.json +++ b/demo/ai-todo-assistant/contexts/task-management/contracts/events/v1/task-created.schema.json @@ -1 +1,30 @@ -{"$schema":"https://json-schema.org/draft/2020-12/schema","title":"TaskCreatedV1","type":"object","additionalProperties":false,"required":["eventId","eventType","schemaVersion","tenantId","actorId","correlationId","causationId","idempotencyKey","taskId","occurredAt"],"properties":{"eventId":{"type":"string"},"eventType":{"const":"TaskCreatedV1"},"schemaVersion":{"const":1},"tenantId":{"type":"string"},"actorId":{"type":"string"},"correlationId":{"type":"string"},"causationId":{"type":"string"},"idempotencyKey":{"type":"string"},"taskId":{"type":"string"},"occurredAt":{"type":"string","format":"date-time"}}} +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "title": "TaskCreatedV1", + "type": "object", + "additionalProperties": false, + "required": [ + "eventId", + "eventType", + "schemaVersion", + "tenantId", + "actorId", + "correlationId", + "causationId", + "idempotencyKey", + "taskId", + "occurredAt" + ], + "properties": { + "eventId": { "type": "string" }, + "eventType": { "const": "TaskCreatedV1" }, + "schemaVersion": { "const": 1 }, + "tenantId": { "type": "string" }, + "actorId": { "type": "string" }, + "correlationId": { "type": "string" }, + "causationId": { "type": "string" }, + "idempotencyKey": { "type": "string" }, + "taskId": { "type": "string" }, + "occurredAt": { "type": "string", "format": "date-time" } + } +} diff --git a/demo/ai-todo-assistant/contexts/task-management/contracts/events/v1/task-deleted.schema.json b/demo/ai-todo-assistant/contexts/task-management/contracts/events/v1/task-deleted.schema.json index e03f2c4..85b3f7d 100644 --- a/demo/ai-todo-assistant/contexts/task-management/contracts/events/v1/task-deleted.schema.json +++ b/demo/ai-todo-assistant/contexts/task-management/contracts/events/v1/task-deleted.schema.json @@ -1 +1,30 @@ -{"$schema":"https://json-schema.org/draft/2020-12/schema","title":"TaskDeletedV1","type":"object","additionalProperties":false,"required":["eventId","eventType","schemaVersion","tenantId","actorId","correlationId","causationId","idempotencyKey","taskId","occurredAt"],"properties":{"eventId":{"type":"string"},"eventType":{"const":"TaskDeletedV1"},"schemaVersion":{"const":1},"tenantId":{"type":"string"},"actorId":{"type":"string"},"correlationId":{"type":"string"},"causationId":{"type":"string"},"idempotencyKey":{"type":"string"},"taskId":{"type":"string"},"occurredAt":{"type":"string","format":"date-time"}}} +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "title": "TaskDeletedV1", + "type": "object", + "additionalProperties": false, + "required": [ + "eventId", + "eventType", + "schemaVersion", + "tenantId", + "actorId", + "correlationId", + "causationId", + "idempotencyKey", + "taskId", + "occurredAt" + ], + "properties": { + "eventId": { "type": "string" }, + "eventType": { "const": "TaskDeletedV1" }, + "schemaVersion": { "const": 1 }, + "tenantId": { "type": "string" }, + "actorId": { "type": "string" }, + "correlationId": { "type": "string" }, + "causationId": { "type": "string" }, + "idempotencyKey": { "type": "string" }, + "taskId": { "type": "string" }, + "occurredAt": { "type": "string", "format": "date-time" } + } +} diff --git a/demo/ai-todo-assistant/contexts/task-management/contracts/events/v1/task-status-changed.schema.json b/demo/ai-todo-assistant/contexts/task-management/contracts/events/v1/task-status-changed.schema.json index 69d6a16..8d21191 100644 --- a/demo/ai-todo-assistant/contexts/task-management/contracts/events/v1/task-status-changed.schema.json +++ b/demo/ai-todo-assistant/contexts/task-management/contracts/events/v1/task-status-changed.schema.json @@ -1 +1,36 @@ -{"$schema":"https://json-schema.org/draft/2020-12/schema","title":"TaskStatusChangedV1","type":"object","additionalProperties":false,"required":["eventId","eventType","schemaVersion","tenantId","actorId","correlationId","causationId","idempotencyKey","taskId","previousStatus","status","version","occurredAt"],"properties":{"eventId":{"type":"string"},"eventType":{"const":"TaskStatusChangedV1"},"schemaVersion":{"const":1},"tenantId":{"type":"string"},"actorId":{"type":"string"},"correlationId":{"type":"string"},"causationId":{"type":"string"},"idempotencyKey":{"type":"string"},"taskId":{"type":"string"},"previousStatus":{"enum":["Todo","InProgress","Completed"]},"status":{"enum":["Todo","InProgress","Completed"]},"version":{"type":"integer","minimum":1},"occurredAt":{"type":"string","format":"date-time"}}} +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "title": "TaskStatusChangedV1", + "type": "object", + "additionalProperties": false, + "required": [ + "eventId", + "eventType", + "schemaVersion", + "tenantId", + "actorId", + "correlationId", + "causationId", + "idempotencyKey", + "taskId", + "previousStatus", + "status", + "version", + "occurredAt" + ], + "properties": { + "eventId": { "type": "string" }, + "eventType": { "const": "TaskStatusChangedV1" }, + "schemaVersion": { "const": 1 }, + "tenantId": { "type": "string" }, + "actorId": { "type": "string" }, + "correlationId": { "type": "string" }, + "causationId": { "type": "string" }, + "idempotencyKey": { "type": "string" }, + "taskId": { "type": "string" }, + "previousStatus": { "enum": ["Todo", "InProgress", "Completed"] }, + "status": { "enum": ["Todo", "InProgress", "Completed"] }, + "version": { "type": "integer", "minimum": 1 }, + "occurredAt": { "type": "string", "format": "date-time" } + } +} diff --git a/demo/ai-todo-assistant/contexts/task-management/contracts/v1/task-cache.schema.json b/demo/ai-todo-assistant/contexts/task-management/contracts/v1/task-cache.schema.json new file mode 100644 index 0000000..ab86a9d --- /dev/null +++ b/demo/ai-todo-assistant/contexts/task-management/contracts/v1/task-cache.schema.json @@ -0,0 +1,6 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "title": "TaskCacheTagV1", + "type": "string", + "enum": ["task-management/v1/task-collection"] +} diff --git a/demo/ai-todo-assistant/contexts/task-management/contracts/v1/task-cache.ts b/demo/ai-todo-assistant/contexts/task-management/contracts/v1/task-cache.ts new file mode 100644 index 0000000..e0f5d2a --- /dev/null +++ b/demo/ai-todo-assistant/contexts/task-management/contracts/v1/task-cache.ts @@ -0,0 +1,7 @@ +export enum TaskCacheTag { + Collection = "task-management/v1/task-collection", +} + +export enum TaskQueryName { + List = "task-management/v1/list-tasks", +} diff --git a/demo/ai-todo-assistant/contexts/task-management/contracts/v1/task-events.ts b/demo/ai-todo-assistant/contexts/task-management/contracts/v1/task-events.ts new file mode 100644 index 0000000..48d46aa --- /dev/null +++ b/demo/ai-todo-assistant/contexts/task-management/contracts/v1/task-events.ts @@ -0,0 +1,51 @@ +import { DomainEventType } from "../../../../core/shared/architecture-enums.js"; + +export interface TaskCollectionEventV1 { + eventId: string; + eventType: + | DomainEventType.TaskCreatedV1 + | DomainEventType.TaskStatusChangedV1 + | DomainEventType.TaskDeletedV1; + schemaVersion: 1; + tenantId: string; + actorId: string; + correlationId: string; + causationId: string; + idempotencyKey: string; + taskId: string; + occurredAt: string; + [property: string]: unknown; +} + +const requiredStrings = [ + "eventId", + "tenantId", + "actorId", + "correlationId", + "causationId", + "idempotencyKey", + "taskId", + "occurredAt", +] as const; + +export function acceptsTaskCollectionEvent( + value: { eventType: string; schemaVersion: number; tenantId: string; [property: string]: unknown }, + eventType: TaskCollectionEventV1["eventType"], +): value is TaskCollectionEventV1 { + const commonEnvelopeIsValid = + value.eventType === eventType && + value.schemaVersion === 1 && + requiredStrings.every((property) => typeof value[property] === "string") && + !Number.isNaN(Date.parse(value.occurredAt as string)); + if (!commonEnvelopeIsValid) return false; + if (eventType !== DomainEventType.TaskStatusChangedV1) return true; + const statuses = new Set(["Todo", "InProgress", "Completed"]); + return ( + typeof value.previousStatus === "string" && + statuses.has(value.previousStatus) && + typeof value.status === "string" && + statuses.has(value.status) && + Number.isSafeInteger(value.version) && + (value.version as number) > 0 + ); +} diff --git a/demo/ai-todo-assistant/contexts/task-management/contracts/v1/task-taxonomy.ts b/demo/ai-todo-assistant/contexts/task-management/contracts/v1/task-taxonomy.ts new file mode 100644 index 0000000..8230e1f --- /dev/null +++ b/demo/ai-todo-assistant/contexts/task-management/contracts/v1/task-taxonomy.ts @@ -0,0 +1,18 @@ +/** Published Task Management vocabulary. Values mirror task-taxonomy.schema.json. */ +export enum TaskCategory { + Work = "Work", + Personal = "Personal", + Sport = "Sport", + Shopping = "Shopping", + Other = "Other", +} +export enum TaskPriority { + Low = "Low", + Medium = "Medium", + High = "High", +} +export enum TaskStatus { + Todo = "Todo", + InProgress = "InProgress", + Completed = "Completed", +} diff --git a/demo/ai-todo-assistant/contexts/task-management/contracts/v1/task-view.schema.json b/demo/ai-todo-assistant/contexts/task-management/contracts/v1/task-view.schema.json index 613b5dd..874d45c 100644 --- a/demo/ai-todo-assistant/contexts/task-management/contracts/v1/task-view.schema.json +++ b/demo/ai-todo-assistant/contexts/task-management/contracts/v1/task-view.schema.json @@ -4,7 +4,17 @@ "title": "TaskView V1", "type": "object", "additionalProperties": false, - "required": ["id", "title", "category", "priority", "status", "version", "createdAt", "updatedAt", "completedAt"], + "required": [ + "id", + "title", + "category", + "priority", + "status", + "version", + "createdAt", + "updatedAt", + "completedAt" + ], "properties": { "id": { "type": "string", "minLength": 1 }, "title": { "type": "string", "minLength": 1, "maxLength": 240 }, diff --git a/demo/ai-todo-assistant/contexts/task-management/contracts/v1/task-view.ts b/demo/ai-todo-assistant/contexts/task-management/contracts/v1/task-view.ts new file mode 100644 index 0000000..c85701f --- /dev/null +++ b/demo/ai-todo-assistant/contexts/task-management/contracts/v1/task-view.ts @@ -0,0 +1,8 @@ +import type { Task } from "../../domain/task.js"; +import type { TaskViewV1 } from "../../../../core/shared/generated/contracts.js"; + +export type TaskView = TaskViewV1; + +export function toTaskView({ tenantId: _tenantId, ...view }: Task): TaskView { + return view; +} diff --git a/demo/ai-todo-assistant/contexts/task-management/contracts/v1/task.schema.json b/demo/ai-todo-assistant/contexts/task-management/contracts/v1/task.schema.json index 2cac399..adc73c6 100644 --- a/demo/ai-todo-assistant/contexts/task-management/contracts/v1/task.schema.json +++ b/demo/ai-todo-assistant/contexts/task-management/contracts/v1/task.schema.json @@ -4,7 +4,18 @@ "title": "Task V1", "type": "object", "additionalProperties": false, - "required": ["id", "tenantId", "title", "category", "priority", "status", "version", "createdAt", "updatedAt", "completedAt"], + "required": [ + "id", + "tenantId", + "title", + "category", + "priority", + "status", + "version", + "createdAt", + "updatedAt", + "completedAt" + ], "properties": { "id": { "type": "string", "minLength": 1 }, "tenantId": { "type": "string", "minLength": 1 }, diff --git a/demo/ai-todo-assistant/contexts/task-management/domain/task-capabilities.ts b/demo/ai-todo-assistant/contexts/task-management/domain/task-capabilities.ts new file mode 100644 index 0000000..aec1c19 --- /dev/null +++ b/demo/ai-todo-assistant/contexts/task-management/domain/task-capabilities.ts @@ -0,0 +1,22 @@ +import type { Task } from "./task.js"; +import type { TaskCategory, TaskStatus } from "./task.js"; +import type { PlatformCapabilities } from "../../../core/shared/aifa.js"; + +export interface TaskPersistenceCapabilities { + TaskCreate(input: { task: Omit }): Promise; + TaskList(input: { + category?: TaskCategory; + status?: TaskStatus; + cursor?: { createdAt: string; id: string }; + limit: number; + }): Promise<{ tasks: Task[]; nextCursor?: string }>; + TaskLoad(input: { taskId: string }): Promise; + TaskSave(input: { + taskId: string; + expectedVersion: number; + changes: Pick; + }): Promise; + TaskDelete(input: { taskId: string; expectedVersion: number }): Promise; +} + +export type TaskCapabilities = TaskPersistenceCapabilities & PlatformCapabilities; diff --git a/demo/ai-todo-assistant/contexts/task-management/domain/task.ts b/demo/ai-todo-assistant/contexts/task-management/domain/task.ts new file mode 100644 index 0000000..04db1bc --- /dev/null +++ b/demo/ai-todo-assistant/contexts/task-management/domain/task.ts @@ -0,0 +1,46 @@ +export { TaskCategory, TaskPriority, TaskStatus } from "../contracts/v1/task-taxonomy.js"; +import { + TaskStatus, + type TaskCategory, + type TaskPriority, +} from "../contracts/v1/task-taxonomy.js"; + +export interface Task { + id: string; + tenantId: string; + title: string; + category: TaskCategory; + priority: TaskPriority; + status: TaskStatus; + version: number; + createdAt: string; + updatedAt: string; + completedAt: string | null; +} + +export type TaskTransitionResult = + | { ok: true; task: Task } + | { ok: false; from: TaskStatus; to: TaskStatus }; + +const allowedTransitions: Readonly> = { + [TaskStatus.Todo]: [TaskStatus.InProgress, TaskStatus.Completed], + [TaskStatus.InProgress]: [TaskStatus.Todo, TaskStatus.Completed], + [TaskStatus.Completed]: [TaskStatus.Todo], +}; + +export function transitionTaskStatus( + task: Task, + status: TaskStatus, + now: string, +): TaskTransitionResult { + if (task.status === status) return { ok: true, task }; + if (!allowedTransitions[task.status].includes(status)) + return { ok: false, from: task.status, to: status }; + return { ok: true, task: { + ...task, + status, + version: task.version + 1, + updatedAt: now, + completedAt: status === TaskStatus.Completed ? now : null, + } }; +} diff --git a/demo/ai-todo-assistant/contexts/task-management/features/change-task-status/backend/feature.ts b/demo/ai-todo-assistant/contexts/task-management/features/change-task-status/backend/feature.ts new file mode 100644 index 0000000..6a86ca8 --- /dev/null +++ b/demo/ai-todo-assistant/contexts/task-management/features/change-task-status/backend/feature.ts @@ -0,0 +1,72 @@ +import { + CapabilityName, + DomainEventType, + ErrorCode, +} from "../../../../../core/shared/architecture-enums.js"; +import { FeatureName } from "../../../../../core/shared/generated/feature-names.js"; +import type { AifaFeature } from "../../../../../core/shared/aifa.js"; +import { TaskStatus, transitionTaskStatus } from "../../../domain/task.js"; +import type { TaskCapabilities } from "../../../domain/task-capabilities.js"; +import { toTaskView, type TaskView } from "../../../contracts/v1/task-view.js"; +export interface ChangeTaskStatusInput { + taskId: string; + status: TaskStatus; + expectedVersion: number; +} +type ChangeStatusCapabilities = Pick< + TaskCapabilities, + "TaskLoad" | "TaskSave" | "ClockNow" | "DomainEventEmit" +>; +export const feature: AifaFeature = { + name: FeatureName.ChangeTaskStatus, + capabilities: [ + CapabilityName.TaskLoad, + CapabilityName.TaskSave, + CapabilityName.ClockNow, + CapabilityName.DomainEventEmit, + ], + async execute(context) { + const task = await context.capabilities.TaskLoad({ taskId: context.input.taskId }); + if (!task) return context.fail(ErrorCode.NotFound, "Task was not found"); + if (task.version !== context.input.expectedVersion) + return context.fail(ErrorCode.VersionConflict, "Task changed", { task: toTaskView(task) }); + const transition = transitionTaskStatus( + task, + context.input.status, + await context.capabilities.ClockNow(), + ); + if (!transition.ok) + return context.fail(ErrorCode.InvalidStatusTransition, "Task status transition is not allowed", { + from: transition.from, + to: transition.to, + }); + const next = transition.task; + if (next === task) return context.ok({ task: toTaskView(task) }); + const saved = await context.capabilities.TaskSave({ + taskId: task.id, + expectedVersion: task.version, + changes: { + status: next.status, + version: next.version, + updatedAt: next.updatedAt, + completedAt: next.completedAt, + }, + }); + if (!saved) { + const current = await context.capabilities.TaskLoad({ taskId: task.id }); + return context.fail(ErrorCode.VersionConflict, "Task changed", { + ...(current ? { task: toTaskView(current) } : {}), + }); + } + await context.capabilities.DomainEventEmit({ + eventType: DomainEventType.TaskStatusChangedV1, + data: { + taskId: task.id, + previousStatus: task.status, + status: saved.status, + version: saved.version, + }, + }); + return context.ok({ task: toTaskView(saved) }); + }, +}; diff --git a/demo/ai-todo-assistant/contexts/task-management/features/change-task-status/contracts/input.schema.json b/demo/ai-todo-assistant/contexts/task-management/features/change-task-status/contracts/input.schema.json index 99f2265..6562c89 100644 --- a/demo/ai-todo-assistant/contexts/task-management/features/change-task-status/contracts/input.schema.json +++ b/demo/ai-todo-assistant/contexts/task-management/features/change-task-status/contracts/input.schema.json @@ -1 +1,12 @@ -{"$schema":"https://json-schema.org/draft/2020-12/schema","type":"object","additionalProperties":false,"required":["taskId","status","expectedVersion","idempotencyKey"],"properties":{"taskId":{"type":"string","minLength":1},"status":{"enum":["Todo","InProgress","Completed"]},"expectedVersion":{"type":"integer","minimum":1},"idempotencyKey":{"type":"string","minLength":16,"maxLength":128}}} +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "title": "ChangeTaskStatusInputV1", + "type": "object", + "additionalProperties": false, + "required": ["taskId", "status", "expectedVersion"], + "properties": { + "taskId": { "type": "string", "minLength": 1 }, + "status": { "enum": ["Todo", "InProgress", "Completed"] }, + "expectedVersion": { "type": "integer", "minimum": 1 } + } +} diff --git a/demo/ai-todo-assistant/contexts/task-management/features/change-task-status/contracts/output.schema.json b/demo/ai-todo-assistant/contexts/task-management/features/change-task-status/contracts/output.schema.json index 42eb5c4..6ccc3d9 100644 --- a/demo/ai-todo-assistant/contexts/task-management/features/change-task-status/contracts/output.schema.json +++ b/demo/ai-todo-assistant/contexts/task-management/features/change-task-status/contracts/output.schema.json @@ -1 +1,10 @@ -{"$schema":"https://json-schema.org/draft/2020-12/schema","type":"object","additionalProperties":false,"required":["task"],"properties":{"task":{"$ref":"https://aifa.dev/task-management/contracts/v1/task-view.schema.json"}}} +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "title": "ChangeTaskStatusOutputV1", + "type": "object", + "additionalProperties": false, + "required": ["task"], + "properties": { + "task": { "$ref": "https://aifa.dev/task-management/contracts/v1/task-view.schema.json" } + } +} diff --git a/demo/ai-todo-assistant/contexts/task-management/features/change-task-status/feature.definition.json b/demo/ai-todo-assistant/contexts/task-management/features/change-task-status/feature.definition.json index fbc5fe4..1468ef8 100644 --- a/demo/ai-todo-assistant/contexts/task-management/features/change-task-status/feature.definition.json +++ b/demo/ai-todo-assistant/contexts/task-management/features/change-task-status/feature.definition.json @@ -4,15 +4,55 @@ "boundedContext": "task-management", "businessNeed": "Keep task lists trustworthy by letting people and AI explicitly move work through Todo, InProgress, and Completed states.", "dependsOn": [], - "consumesContracts": [], - "backend": { "route": "/api/tasks/:taskId/status", "method": "PATCH", "capabilities": ["TaskLoad", "TaskSave", "ClockNow", "AuditRecord"], "mcp": { "exposed": true, "toolName": "change_task_status" } }, - "frontend": { "slots": ["TaskRowActions"] }, - "contracts": { "input": "contracts/input.schema.json", "output": "contracts/output.schema.json", "mcpInput": "contracts/input.schema.json" }, + "consumesContracts": [ + "task-management/contracts/v1/task-cache.schema.json", + "task-management/contracts/events/v1/task-status-changed.schema.json" + ], + "backend": { + "route": "/api/tasks/:taskId/status", + "method": "PATCH", + "capabilities": ["TaskLoad", "TaskSave", "ClockNow", "DomainEventEmit"], + "mcp": { "exposed": true, "toolName": "change_task_status" } + }, + "frontend": { + "slots": ["TaskRowActions"], + "eventConsumers": [ + { + "name": "invalidate-task-collection-after-status-change", + "eventType": "TaskStatusChangedV1", + "contract": "task-management/contracts/events/v1/task-status-changed.schema.json" + } + ] + }, + "contracts": { + "input": "contracts/input.schema.json", + "output": "contracts/output.schema.json", + "mcpInput": "contracts/input.schema.json" + }, "events": ["task-management/contracts/events/v1/task-status-changed.schema.json"], - "security": { "mutation": true, "actorRequired": true, "idempotent": true, "requiresConfirmation": false, "concurrency": "optimistic", "mcpScopes": ["TaskWrite"] }, + "security": { + "mutation": true, + "actorRequired": true, + "idempotent": true, + "requiresConfirmation": false, + "concurrency": "optimistic", + "mcpScopes": ["TaskWrite"] + }, "workItems": [ - { "id": "STS-1", "description": "Model allowed TaskStatus transitions as a task-management domain invariant.", "acceptanceCriterion": "Invalid transitions return InvalidStatusTransition." }, - { "id": "STS-2", "description": "Implement the status-transition feature and audit event mapping.", "acceptanceCriterion": "Completion timestamps are set and cleared correctly." }, - { "id": "STS-3", "description": "Register MUI and MCP status controls.", "acceptanceCriterion": "HTTP and MCP calls produce the same validated result." } + { + "id": "STS-1", + "description": "Model allowed TaskStatus transitions as a task-management domain invariant.", + "acceptanceCriterion": "Invalid transitions return InvalidStatusTransition." + }, + { + "id": "STS-2", + "description": "Implement the status-transition feature and audit event mapping.", + "acceptanceCriterion": "Completion timestamps are set and cleared correctly." + }, + { + "id": "STS-3", + "description": "Register MUI and MCP status controls.", + "acceptanceCriterion": "HTTP and MCP calls produce the same validated result." + } ] } diff --git a/demo/ai-todo-assistant/contexts/task-management/features/change-task-status/frontend/contribution.tsx b/demo/ai-todo-assistant/contexts/task-management/features/change-task-status/frontend/contribution.tsx new file mode 100644 index 0000000..f6a23f0 --- /dev/null +++ b/demo/ai-todo-assistant/contexts/task-management/features/change-task-status/frontend/contribution.tsx @@ -0,0 +1,78 @@ +import { Alert, Button, Menu, MenuItem, Stack } from "@mui/material"; +import { useState } from "react"; +import { TaskStatus } from "../../../domain/task.js"; +import type { TaskRowActionsModel } from "../../../../../core/frontend/app-model.js"; +import type { FrontendEventConsumer } from "../../../../../core/frontend/events/event-registry.js"; +import type { SlotContribution } from "../../../../../core/frontend/slots/slot-registry.js"; +import { DomainEventType, SlotName } from "../../../../../core/shared/architecture-enums.js"; +import { TaskCacheTag } from "../../../contracts/v1/task-cache.js"; +import { + acceptsTaskCollectionEvent, + type TaskCollectionEventV1, +} from "../../../contracts/v1/task-events.js"; +import { useChangeTaskStatus } from "./use-change-task-status.js"; +function StatusControl(model: TaskRowActionsModel) { + const { task } = model; + const [anchor, setAnchor] = useState(null); + const changeStatus = useChangeTaskStatus(); + const nextStatus = task.status === TaskStatus.Todo ? TaskStatus.InProgress : TaskStatus.Completed; + return ( + + {changeStatus.error && {changeStatus.error.message}} + {task.status !== TaskStatus.Completed && ( + + )} + + setAnchor(null)}> + {Object.values(TaskStatus).map((status) => ( + { + setAnchor(null); + if (status !== task.status) void changeStatus.mutate({ task, status }); + }} + > + {status === TaskStatus.Todo + ? "To do" + : status === TaskStatus.InProgress + ? "In progress" + : "Done"} + + ))} + + + ); +} +export const contribution: SlotContribution = { + slot: SlotName.TaskRowActions, + name: "change-task-status", + order: 10, + render: (model) => , +}; + +export const eventConsumers: FrontendEventConsumer[] = [ + { + name: "invalidate-task-collection-after-status-change", + eventType: DomainEventType.TaskStatusChangedV1, + contract: "task-management/contracts/events/v1/task-status-changed.schema.json", + accepts: (event): event is TaskCollectionEventV1 => + acceptsTaskCollectionEvent(event, DomainEventType.TaskStatusChangedV1), + handle: (_event, { queryClient }) => + queryClient.invalidateTags([TaskCacheTag.Collection]), + }, +]; diff --git a/demo/ai-todo-assistant/contexts/task-management/features/change-task-status/frontend/use-change-task-status.ts b/demo/ai-todo-assistant/contexts/task-management/features/change-task-status/frontend/use-change-task-status.ts new file mode 100644 index 0000000..a962013 --- /dev/null +++ b/demo/ai-todo-assistant/contexts/task-management/features/change-task-status/frontend/use-change-task-status.ts @@ -0,0 +1,25 @@ +import { api } from "../../../../../core/frontend/api.js"; +import { useMutation, useQueryClient } from "../../../../../core/frontend/query/react-query.js"; +import type { TaskViewV1 } from "../../../../../core/shared/generated/contracts.js"; +import { TaskCacheTag } from "../../../contracts/v1/task-cache.js"; +import type { TaskStatus } from "../../../contracts/v1/task-taxonomy.js"; + +interface ChangeTaskStatusInput { + task: TaskViewV1; + status: TaskStatus; +} + +export function useChangeTaskStatus() { + const queryClient = useQueryClient(); + return useMutation({ + mutationFn: async ({ task, status }) => + ( + await api<{ task: TaskViewV1 }>(`/api/tasks/${task.id}/status`, { + method: "PATCH", + headers: { "idempotency-key": crypto.randomUUID() }, + body: JSON.stringify({ status, expectedVersion: task.version }), + }) + ).task, + onSuccess: () => queryClient.invalidateTags([TaskCacheTag.Collection]), + }); +} diff --git a/demo/ai-todo-assistant/contexts/task-management/features/change-task-status/manifest.ts b/demo/ai-todo-assistant/contexts/task-management/features/change-task-status/manifest.ts new file mode 100644 index 0000000..9bdfe4f --- /dev/null +++ b/demo/ai-todo-assistant/contexts/task-management/features/change-task-status/manifest.ts @@ -0,0 +1,28 @@ +import { + DomainEventType, + HttpMethod, + PermissionScope, + SlotName, +} from "../../../../core/shared/architecture-enums.js"; +import { feature } from "./backend/feature.js"; +import type { FeatureManifest } from "../../../../core/shared/feature-manifest.js"; +export const manifest = { + name: feature.name, + backend: { + method: HttpMethod.Patch, + route: "/api/tasks/:taskId/status", + feature, + requiredScopes: [PermissionScope.TaskWrite], + }, + frontend: { + contributions: [{ slot: SlotName.TaskRowActions, name: "change-task-status" }], + eventConsumers: [ + { + name: "invalidate-task-collection-after-status-change", + eventType: DomainEventType.TaskStatusChangedV1, + contract: "task-management/contracts/events/v1/task-status-changed.schema.json", + }, + ], + }, + mcp: { toolName: "change_task_status" }, +} satisfies FeatureManifest; diff --git a/demo/ai-todo-assistant/contexts/task-management/features/create-task/backend/feature.ts b/demo/ai-todo-assistant/contexts/task-management/features/create-task/backend/feature.ts new file mode 100644 index 0000000..836f23e --- /dev/null +++ b/demo/ai-todo-assistant/contexts/task-management/features/create-task/backend/feature.ts @@ -0,0 +1,51 @@ +import { + CapabilityName, + DomainEventType, + ErrorCode, +} from "../../../../../core/shared/architecture-enums.js"; +import { FeatureName } from "../../../../../core/shared/generated/feature-names.js"; +import type { AifaFeature } from "../../../../../core/shared/aifa.js"; +import { TaskCategory, TaskPriority, TaskStatus, type Task } from "../../../domain/task.js"; +import type { TaskCapabilities } from "../../../domain/task-capabilities.js"; +import { toTaskView, type TaskView } from "../../../contracts/v1/task-view.js"; + +export interface CreateTaskInput { + title: string; + category: TaskCategory; + priority: TaskPriority; +} +type CreateTaskCapabilities = Pick< + TaskCapabilities, + "TaskCreate" | "IdCreate" | "ClockNow" | "DomainEventEmit" +>; +export const feature: AifaFeature = { + name: FeatureName.CreateTask, + capabilities: [ + CapabilityName.TaskCreate, + CapabilityName.IdCreate, + CapabilityName.ClockNow, + CapabilityName.DomainEventEmit, + ], + async execute(context) { + const title = context.input.title.trim(); + if (!title) return context.fail(ErrorCode.InvalidInput, "Task title is required"); + const now = await context.capabilities.ClockNow(); + const task: Omit = { + id: await context.capabilities.IdCreate(), + title, + category: context.input.category, + priority: context.input.priority, + status: TaskStatus.Todo, + version: 1, + createdAt: now, + updatedAt: now, + completedAt: null, + }; + const created = await context.capabilities.TaskCreate({ task }); + await context.capabilities.DomainEventEmit({ + eventType: DomainEventType.TaskCreatedV1, + data: { taskId: created.id }, + }); + return context.ok({ task: toTaskView(created) }); + }, +}; diff --git a/demo/ai-todo-assistant/contexts/task-management/features/create-task/contracts/input.schema.json b/demo/ai-todo-assistant/contexts/task-management/features/create-task/contracts/input.schema.json index 22b7780..acc8087 100644 --- a/demo/ai-todo-assistant/contexts/task-management/features/create-task/contracts/input.schema.json +++ b/demo/ai-todo-assistant/contexts/task-management/features/create-task/contracts/input.schema.json @@ -1 +1,12 @@ -{"$schema":"https://json-schema.org/draft/2020-12/schema","type":"object","additionalProperties":false,"required":["title","category","priority","idempotencyKey"],"properties":{"title":{"type":"string","minLength":1,"maxLength":240},"category":{"enum":["Work","Personal","Sport","Shopping","Other"]},"priority":{"enum":["Low","Medium","High"]},"idempotencyKey":{"type":"string","minLength":16,"maxLength":128}}} +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "title": "CreateTaskInputV1", + "type": "object", + "additionalProperties": false, + "required": ["title", "category", "priority"], + "properties": { + "title": { "type": "string", "minLength": 1, "maxLength": 240 }, + "category": { "enum": ["Work", "Personal", "Sport", "Shopping", "Other"] }, + "priority": { "enum": ["Low", "Medium", "High"] } + } +} diff --git a/demo/ai-todo-assistant/contexts/task-management/features/create-task/contracts/output.schema.json b/demo/ai-todo-assistant/contexts/task-management/features/create-task/contracts/output.schema.json index 42eb5c4..ee7d45b 100644 --- a/demo/ai-todo-assistant/contexts/task-management/features/create-task/contracts/output.schema.json +++ b/demo/ai-todo-assistant/contexts/task-management/features/create-task/contracts/output.schema.json @@ -1 +1,10 @@ -{"$schema":"https://json-schema.org/draft/2020-12/schema","type":"object","additionalProperties":false,"required":["task"],"properties":{"task":{"$ref":"https://aifa.dev/task-management/contracts/v1/task-view.schema.json"}}} +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "title": "CreateTaskOutputV1", + "type": "object", + "additionalProperties": false, + "required": ["task"], + "properties": { + "task": { "$ref": "https://aifa.dev/task-management/contracts/v1/task-view.schema.json" } + } +} diff --git a/demo/ai-todo-assistant/contexts/task-management/features/create-task/feature.definition.json b/demo/ai-todo-assistant/contexts/task-management/features/create-task/feature.definition.json index 22f1ac6..4c66993 100644 --- a/demo/ai-todo-assistant/contexts/task-management/features/create-task/feature.definition.json +++ b/demo/ai-todo-assistant/contexts/task-management/features/create-task/feature.definition.json @@ -4,15 +4,55 @@ "boundedContext": "task-management", "businessNeed": "Capture a categorized commitment quickly and consistently, whether it originates from a person or an accepted AI suggestion.", "dependsOn": [], - "consumesContracts": [], - "backend": { "route": "/api/tasks", "method": "POST", "capabilities": ["TaskCreate", "IdCreate", "ClockNow", "AuditRecord"], "mcp": { "exposed": true, "toolName": "create_task" } }, - "frontend": { "slots": ["TaskComposer"] }, - "contracts": { "input": "contracts/input.schema.json", "output": "contracts/output.schema.json", "mcpInput": "contracts/input.schema.json" }, + "consumesContracts": [ + "task-management/contracts/v1/task-cache.schema.json", + "task-management/contracts/events/v1/task-created.schema.json" + ], + "backend": { + "route": "/api/tasks", + "method": "POST", + "capabilities": ["TaskCreate", "IdCreate", "ClockNow", "DomainEventEmit"], + "mcp": { "exposed": true, "toolName": "create_task" } + }, + "frontend": { + "slots": ["TaskComposer"], + "eventConsumers": [ + { + "name": "invalidate-task-collection-after-create", + "eventType": "TaskCreatedV1", + "contract": "task-management/contracts/events/v1/task-created.schema.json" + } + ] + }, + "contracts": { + "input": "contracts/input.schema.json", + "output": "contracts/output.schema.json", + "mcpInput": "contracts/input.schema.json" + }, "events": ["task-management/contracts/events/v1/task-created.schema.json"], - "security": { "mutation": true, "actorRequired": true, "idempotent": true, "requiresConfirmation": false, "concurrency": "none", "mcpScopes": ["TaskWrite"] }, + "security": { + "mutation": true, + "actorRequired": true, + "idempotent": true, + "requiresConfirmation": false, + "concurrency": "none", + "mcpScopes": ["TaskWrite"] + }, "workItems": [ - { "id": "CRT-1", "description": "Define validated create-task input using TaskCategory and TaskPriority enums.", "acceptanceCriterion": "Invalid category, priority, or blank title returns a typed failure." }, - { "id": "CRT-2", "description": "Implement and test the AIFA feature using only declared capabilities.", "acceptanceCriterion": "A task is persisted as Todo and audited." }, - { "id": "CRT-3", "description": "Register the MUI composer slot contribution.", "acceptanceCriterion": "A submitted form refreshes the typed task model." } + { + "id": "CRT-1", + "description": "Define validated create-task input using TaskCategory and TaskPriority enums.", + "acceptanceCriterion": "Invalid category, priority, or blank title returns a typed failure." + }, + { + "id": "CRT-2", + "description": "Implement and test the AIFA feature using only declared capabilities.", + "acceptanceCriterion": "A task is persisted as Todo and audited." + }, + { + "id": "CRT-3", + "description": "Register the MUI composer slot contribution.", + "acceptanceCriterion": "A submitted form refreshes the typed task model." + } ] } diff --git a/demo/ai-todo-assistant/contexts/task-management/features/create-task/frontend/contribution.tsx b/demo/ai-todo-assistant/contexts/task-management/features/create-task/frontend/contribution.tsx new file mode 100644 index 0000000..7d82148 --- /dev/null +++ b/demo/ai-todo-assistant/contexts/task-management/features/create-task/frontend/contribution.tsx @@ -0,0 +1,101 @@ +import Add from "@mui/icons-material/Add"; +import { Alert, Box, Button, MenuItem, Select, Stack, TextField } from "@mui/material"; +import { useState } from "react"; +import { TaskCategory, TaskPriority } from "../../../domain/task.js"; +import type { FrontendEventConsumer } from "../../../../../core/frontend/events/event-registry.js"; +import type { SlotContribution } from "../../../../../core/frontend/slots/slot-registry.js"; +import { DomainEventType, SlotName } from "../../../../../core/shared/architecture-enums.js"; +import { TaskCacheTag } from "../../../contracts/v1/task-cache.js"; +import { + acceptsTaskCollectionEvent, + type TaskCollectionEventV1, +} from "../../../contracts/v1/task-events.js"; +import { useCreateTask } from "./use-create-task.js"; + +function CreateTaskForm() { + const [title, setTitle] = useState(""); + const [category, setCategory] = useState(TaskCategory.Work); + const [priority, setPriority] = useState(TaskPriority.Medium); + const createTask = useCreateTask(); + return ( + + {createTask.error && {createTask.error.message}} + + setTitle(event.target.value)} + sx={{ "& .MuiInputBase-root": { height: 38 }, "& input": { py: 0.9, fontSize: 14 } }} + /> + + + + + + ); +} +export const contribution: SlotContribution = { + slot: SlotName.TaskComposer, + name: "create-task-form", + render: () => , +}; + +export const eventConsumers: FrontendEventConsumer[] = [ + { + name: "invalidate-task-collection-after-create", + eventType: DomainEventType.TaskCreatedV1, + contract: "task-management/contracts/events/v1/task-created.schema.json", + accepts: (event): event is TaskCollectionEventV1 => + acceptsTaskCollectionEvent(event, DomainEventType.TaskCreatedV1), + handle: (_event, { queryClient }) => + queryClient.invalidateTags([TaskCacheTag.Collection]), + }, +]; diff --git a/demo/ai-todo-assistant/contexts/task-management/features/create-task/frontend/use-create-task.ts b/demo/ai-todo-assistant/contexts/task-management/features/create-task/frontend/use-create-task.ts new file mode 100644 index 0000000..b03b86f --- /dev/null +++ b/demo/ai-todo-assistant/contexts/task-management/features/create-task/frontend/use-create-task.ts @@ -0,0 +1,26 @@ +import { api } from "../../../../../core/frontend/api.js"; +import { useMutation, useQueryClient } from "../../../../../core/frontend/query/react-query.js"; +import type { TaskViewV1 } from "../../../../../core/shared/generated/contracts.js"; +import { TaskCacheTag } from "../../../contracts/v1/task-cache.js"; +import type { TaskCategory, TaskPriority } from "../../../contracts/v1/task-taxonomy.js"; + +export interface CreateTaskInput { + title: string; + category: TaskCategory; + priority: TaskPriority; +} + +export function useCreateTask() { + const queryClient = useQueryClient(); + return useMutation({ + mutationFn: async (input) => + ( + await api<{ task: TaskViewV1 }>("/api/tasks", { + method: "POST", + headers: { "idempotency-key": crypto.randomUUID() }, + body: JSON.stringify(input), + }) + ).task, + onSuccess: () => queryClient.invalidateTags([TaskCacheTag.Collection]), + }); +} diff --git a/demo/ai-todo-assistant/contexts/task-management/features/create-task/manifest.ts b/demo/ai-todo-assistant/contexts/task-management/features/create-task/manifest.ts new file mode 100644 index 0000000..b4ed42f --- /dev/null +++ b/demo/ai-todo-assistant/contexts/task-management/features/create-task/manifest.ts @@ -0,0 +1,28 @@ +import { + DomainEventType, + HttpMethod, + PermissionScope, + SlotName, +} from "../../../../core/shared/architecture-enums.js"; +import { feature } from "./backend/feature.js"; +import type { FeatureManifest } from "../../../../core/shared/feature-manifest.js"; +export const manifest = { + name: feature.name, + backend: { + method: HttpMethod.Post, + route: "/api/tasks", + feature, + requiredScopes: [PermissionScope.TaskWrite], + }, + frontend: { + contributions: [{ slot: SlotName.TaskComposer, name: "create-task-form" }], + eventConsumers: [ + { + name: "invalidate-task-collection-after-create", + eventType: DomainEventType.TaskCreatedV1, + contract: "task-management/contracts/events/v1/task-created.schema.json", + }, + ], + }, + mcp: { toolName: "create_task" }, +} satisfies FeatureManifest; diff --git a/demo/ai-todo-assistant/contexts/task-management/features/delete-task/backend/feature.ts b/demo/ai-todo-assistant/contexts/task-management/features/delete-task/backend/feature.ts new file mode 100644 index 0000000..4cff04d --- /dev/null +++ b/demo/ai-todo-assistant/contexts/task-management/features/delete-task/backend/feature.ts @@ -0,0 +1,44 @@ +import { + CapabilityName, + DomainEventType, + ErrorCode, +} from "../../../../../core/shared/architecture-enums.js"; +import { FeatureName } from "../../../../../core/shared/generated/feature-names.js"; +import type { AifaFeature } from "../../../../../core/shared/aifa.js"; +import type { TaskCapabilities } from "../../../domain/task-capabilities.js"; +import type { + DeleteTaskInputV1, + DeleteTaskOutputV1, +} from "../../../../../core/shared/generated/contracts.js"; +export type DeleteTaskInput = DeleteTaskInputV1; +type DeleteTaskCapabilities = Pick< + TaskCapabilities, + "TaskLoad" | "TaskDelete" | "DomainEventEmit" +>; +export const feature: AifaFeature = { + name: FeatureName.DeleteTask, + capabilities: [CapabilityName.TaskLoad, CapabilityName.TaskDelete, CapabilityName.DomainEventEmit], + async execute(context) { + if (!context.input.confirmed) + return context.fail(ErrorCode.ConfirmationRequired, "Deletion requires confirmation"); + const current = await context.capabilities.TaskLoad({ taskId: context.input.taskId }); + if (!current) return context.fail(ErrorCode.NotFound, "Task was not found"); + if (current.version !== context.input.expectedVersion) + return context.fail(ErrorCode.VersionConflict, "Task changed", { version: current.version }); + const deleted = await context.capabilities.TaskDelete({ + taskId: current.id, + expectedVersion: current.version, + }); + if (!deleted) { + const latest = await context.capabilities.TaskLoad({ taskId: current.id }); + return context.fail(ErrorCode.VersionConflict, "Task changed", { + ...(latest ? { version: latest.version } : {}), + }); + } + await context.capabilities.DomainEventEmit({ + eventType: DomainEventType.TaskDeletedV1, + data: { taskId: current.id }, + }); + return context.ok({ deleted: true }); + }, +}; diff --git a/demo/ai-todo-assistant/contexts/task-management/features/delete-task/contracts/input.schema.json b/demo/ai-todo-assistant/contexts/task-management/features/delete-task/contracts/input.schema.json index 7a59e0b..2448d91 100644 --- a/demo/ai-todo-assistant/contexts/task-management/features/delete-task/contracts/input.schema.json +++ b/demo/ai-todo-assistant/contexts/task-management/features/delete-task/contracts/input.schema.json @@ -1 +1,12 @@ -{"$schema":"https://json-schema.org/draft/2020-12/schema","type":"object","additionalProperties":false,"required":["taskId","expectedVersion","idempotencyKey","confirmed"],"properties":{"taskId":{"type":"string","minLength":1},"expectedVersion":{"type":"integer","minimum":1},"idempotencyKey":{"type":"string","minLength":16,"maxLength":128},"confirmed":{"const":true}}} +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "title": "DeleteTaskInputV1", + "type": "object", + "additionalProperties": false, + "required": ["taskId", "expectedVersion", "confirmed"], + "properties": { + "taskId": { "type": "string", "minLength": 1 }, + "expectedVersion": { "type": "integer", "minimum": 1 }, + "confirmed": { "const": true } + } +} diff --git a/demo/ai-todo-assistant/contexts/task-management/features/delete-task/contracts/output.schema.json b/demo/ai-todo-assistant/contexts/task-management/features/delete-task/contracts/output.schema.json index c5f6d42..2740e17 100644 --- a/demo/ai-todo-assistant/contexts/task-management/features/delete-task/contracts/output.schema.json +++ b/demo/ai-todo-assistant/contexts/task-management/features/delete-task/contracts/output.schema.json @@ -1 +1,8 @@ -{"$schema":"https://json-schema.org/draft/2020-12/schema","type":"object","additionalProperties":false,"required":["deleted"],"properties":{"deleted":{"const":true}}} +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "title": "DeleteTaskOutputV1", + "type": "object", + "additionalProperties": false, + "required": ["deleted"], + "properties": { "deleted": { "const": true } } +} diff --git a/demo/ai-todo-assistant/contexts/task-management/features/delete-task/feature.definition.json b/demo/ai-todo-assistant/contexts/task-management/features/delete-task/feature.definition.json index 5a2961e..49d9e14 100644 --- a/demo/ai-todo-assistant/contexts/task-management/features/delete-task/feature.definition.json +++ b/demo/ai-todo-assistant/contexts/task-management/features/delete-task/feature.definition.json @@ -4,14 +4,50 @@ "boundedContext": "task-management", "businessNeed": "Keep the task list useful by removing incorrect or obsolete commitments with explicit user confirmation.", "dependsOn": [], - "consumesContracts": [], - "backend": { "route": "/api/tasks/:taskId", "method": "DELETE", "capabilities": ["TaskLoad", "TaskDelete", "AuditRecord"], "mcp": { "exposed": true, "toolName": "delete_task" } }, - "frontend": { "slots": ["TaskRowActions"] }, - "contracts": { "input": "contracts/input.schema.json", "output": "contracts/output.schema.json", "mcpInput": "contracts/input.schema.json" }, + "consumesContracts": [ + "task-management/contracts/v1/task-cache.schema.json", + "task-management/contracts/events/v1/task-deleted.schema.json" + ], + "backend": { + "route": "/api/tasks/:taskId", + "method": "DELETE", + "capabilities": ["TaskLoad", "TaskDelete", "DomainEventEmit"], + "mcp": { "exposed": true, "toolName": "delete_task" } + }, + "frontend": { + "slots": ["TaskRowActions"], + "eventConsumers": [ + { + "name": "invalidate-task-collection-after-delete", + "eventType": "TaskDeletedV1", + "contract": "task-management/contracts/events/v1/task-deleted.schema.json" + } + ] + }, + "contracts": { + "input": "contracts/input.schema.json", + "output": "contracts/output.schema.json", + "mcpInput": "contracts/input.schema.json" + }, "events": ["task-management/contracts/events/v1/task-deleted.schema.json"], - "security": { "mutation": true, "actorRequired": true, "idempotent": true, "requiresConfirmation": true, "concurrency": "optimistic", "mcpScopes": ["TaskDelete"] }, + "security": { + "mutation": true, + "actorRequired": true, + "idempotent": true, + "requiresConfirmation": true, + "concurrency": "optimistic", + "mcpScopes": ["TaskDelete"] + }, "workItems": [ - { "id": "DEL-1", "description": "Implement existence-aware deletion and audit behavior.", "acceptanceCriterion": "Unknown IDs return NotFound and do not produce an audit record." }, - { "id": "DEL-2", "description": "Register the confirmed MUI deletion action.", "acceptanceCriterion": "The HTTP request occurs only after dialog confirmation." } + { + "id": "DEL-1", + "description": "Implement existence-aware deletion and audit behavior.", + "acceptanceCriterion": "Unknown IDs return NotFound and do not produce an audit record." + }, + { + "id": "DEL-2", + "description": "Register the confirmed MUI deletion action.", + "acceptanceCriterion": "The HTTP request occurs only after dialog confirmation." + } ] } diff --git a/demo/ai-todo-assistant/contexts/task-management/features/delete-task/frontend/contribution.tsx b/demo/ai-todo-assistant/contexts/task-management/features/delete-task/frontend/contribution.tsx new file mode 100644 index 0000000..5b2539f --- /dev/null +++ b/demo/ai-todo-assistant/contexts/task-management/features/delete-task/frontend/contribution.tsx @@ -0,0 +1,82 @@ +import Delete from "@mui/icons-material/Delete"; +import { + Button, + Alert, + Dialog, + DialogActions, + DialogContent, + DialogContentText, + DialogTitle, + IconButton, +} from "@mui/material"; +import { useState } from "react"; +import type { TaskRowActionsModel } from "../../../../../core/frontend/app-model.js"; +import type { FrontendEventConsumer } from "../../../../../core/frontend/events/event-registry.js"; +import type { SlotContribution } from "../../../../../core/frontend/slots/slot-registry.js"; +import { DomainEventType, SlotName } from "../../../../../core/shared/architecture-enums.js"; +import { TaskCacheTag } from "../../../contracts/v1/task-cache.js"; +import { + acceptsTaskCollectionEvent, + type TaskCollectionEventV1, +} from "../../../contracts/v1/task-events.js"; +import { useDeleteTask } from "./use-delete-task.js"; + +function DeleteTaskAction(model: TaskRowActionsModel) { + const { task } = model; + const [open, setOpen] = useState(false); + const deleteTask = useDeleteTask(); + return ( + <> + {deleteTask.error && {deleteTask.error.message}} + setOpen(true)} + > + + + setOpen(false)} aria-labelledby="delete-task-title"> + Delete task? + + + “{task.title}” will be permanently removed. This action is audited. + + + + + + + + + ); +} + +export const contribution: SlotContribution = { + slot: SlotName.TaskRowActions, + name: "delete-task", + order: 20, + render: (model) => , +}; + +export const eventConsumers: FrontendEventConsumer[] = [ + { + name: "invalidate-task-collection-after-delete", + eventType: DomainEventType.TaskDeletedV1, + contract: "task-management/contracts/events/v1/task-deleted.schema.json", + accepts: (event): event is TaskCollectionEventV1 => + acceptsTaskCollectionEvent(event, DomainEventType.TaskDeletedV1), + handle: (_event, { queryClient }) => + queryClient.invalidateTags([TaskCacheTag.Collection]), + }, +]; diff --git a/demo/ai-todo-assistant/contexts/task-management/features/delete-task/frontend/use-delete-task.ts b/demo/ai-todo-assistant/contexts/task-management/features/delete-task/frontend/use-delete-task.ts new file mode 100644 index 0000000..f99c164 --- /dev/null +++ b/demo/ai-todo-assistant/contexts/task-management/features/delete-task/frontend/use-delete-task.ts @@ -0,0 +1,19 @@ +import { api } from "../../../../../core/frontend/api.js"; +import { useMutation, useQueryClient } from "../../../../../core/frontend/query/react-query.js"; +import type { TaskViewV1 } from "../../../../../core/shared/generated/contracts.js"; +import { TaskCacheTag } from "../../../contracts/v1/task-cache.js"; + +export function useDeleteTask() { + const queryClient = useQueryClient(); + return useMutation({ + mutationFn: async (task) => + ( + await api<{ deleted: true }>(`/api/tasks/${task.id}`, { + method: "DELETE", + headers: { "idempotency-key": crypto.randomUUID() }, + body: JSON.stringify({ expectedVersion: task.version, confirmed: true }), + }) + ).deleted, + onSuccess: () => queryClient.invalidateTags([TaskCacheTag.Collection]), + }); +} diff --git a/demo/ai-todo-assistant/contexts/task-management/features/delete-task/manifest.ts b/demo/ai-todo-assistant/contexts/task-management/features/delete-task/manifest.ts new file mode 100644 index 0000000..3aa1409 --- /dev/null +++ b/demo/ai-todo-assistant/contexts/task-management/features/delete-task/manifest.ts @@ -0,0 +1,28 @@ +import { + DomainEventType, + HttpMethod, + PermissionScope, + SlotName, +} from "../../../../core/shared/architecture-enums.js"; +import { feature } from "./backend/feature.js"; +import type { FeatureManifest } from "../../../../core/shared/feature-manifest.js"; +export const manifest = { + name: feature.name, + backend: { + method: HttpMethod.Delete, + route: "/api/tasks/:taskId", + feature, + requiredScopes: [PermissionScope.TaskDelete], + }, + frontend: { + contributions: [{ slot: SlotName.TaskRowActions, name: "delete-task" }], + eventConsumers: [ + { + name: "invalidate-task-collection-after-delete", + eventType: DomainEventType.TaskDeletedV1, + contract: "task-management/contracts/events/v1/task-deleted.schema.json", + }, + ], + }, + mcp: { toolName: "delete_task" }, +} satisfies FeatureManifest; diff --git a/demo/ai-todo-assistant/contexts/task-management/features/list-tasks/backend/feature.ts b/demo/ai-todo-assistant/contexts/task-management/features/list-tasks/backend/feature.ts new file mode 100644 index 0000000..1008230 --- /dev/null +++ b/demo/ai-todo-assistant/contexts/task-management/features/list-tasks/backend/feature.ts @@ -0,0 +1,45 @@ +import { CapabilityName } from "../../../../../core/shared/architecture-enums.js"; +import { FeatureName } from "../../../../../core/shared/generated/feature-names.js"; +import type { AifaFeature } from "../../../../../core/shared/aifa.js"; +import type { TaskCategory, TaskStatus } from "../../../domain/task.js"; +import type { TaskCapabilities } from "../../../domain/task-capabilities.js"; +import { toTaskView, type TaskView } from "../../../contracts/v1/task-view.js"; +import { ErrorCode } from "../../../../../core/shared/architecture-enums.js"; +interface ListTasksInput { + category?: TaskCategory; + status?: TaskStatus; + cursor?: string; + limit?: number; +} +function decodeCursor(value: string | undefined): { createdAt: string; id: string } | undefined { + if (!value) return undefined; + try { + const cursor = JSON.parse(Buffer.from(value, "base64url").toString("utf8")) as Record; + if (typeof cursor.createdAt === "string" && typeof cursor.id === "string") + return { createdAt: cursor.createdAt, id: cursor.id }; + } catch {} + return undefined; +} +export const feature: AifaFeature< + ListTasksInput, + { tasks: TaskView[]; nextCursor?: string }, + Pick +> = { + name: FeatureName.ListTasks, + capabilities: [CapabilityName.TaskList], + async execute(context) { + const cursor = decodeCursor(context.input.cursor); + if (context.input.cursor && !cursor) + return context.fail(ErrorCode.InvalidInput, "Cursor is invalid"); + const page = await context.capabilities.TaskList({ + category: context.input.category, + status: context.input.status, + cursor, + limit: context.input.limit ?? 50, + }); + return context.ok({ + tasks: page.tasks.map(toTaskView), + ...(page.nextCursor ? { nextCursor: page.nextCursor } : {}), + }); + }, +}; diff --git a/demo/ai-todo-assistant/contexts/task-management/features/list-tasks/contracts/input.schema.json b/demo/ai-todo-assistant/contexts/task-management/features/list-tasks/contracts/input.schema.json index 84e4f78..3704f7f 100644 --- a/demo/ai-todo-assistant/contexts/task-management/features/list-tasks/contracts/input.schema.json +++ b/demo/ai-todo-assistant/contexts/task-management/features/list-tasks/contracts/input.schema.json @@ -1 +1,12 @@ -{"$schema":"https://json-schema.org/draft/2020-12/schema","type":"object","additionalProperties":false,"properties":{"category":{"enum":["Work","Personal","Sport","Shopping","Other"]},"status":{"enum":["Todo","InProgress","Completed"]}}} +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "title": "ListTasksInputV1", + "type": "object", + "additionalProperties": false, + "properties": { + "category": { "enum": ["Work", "Personal", "Sport", "Shopping", "Other"] }, + "status": { "enum": ["Todo", "InProgress", "Completed"] }, + "cursor": { "type": "string", "minLength": 1, "maxLength": 1024 }, + "limit": { "type": "integer", "minimum": 1, "maximum": 100 } + } +} diff --git a/demo/ai-todo-assistant/contexts/task-management/features/list-tasks/contracts/output.schema.json b/demo/ai-todo-assistant/contexts/task-management/features/list-tasks/contracts/output.schema.json index ebeaf14..b26a743 100644 --- a/demo/ai-todo-assistant/contexts/task-management/features/list-tasks/contracts/output.schema.json +++ b/demo/ai-todo-assistant/contexts/task-management/features/list-tasks/contracts/output.schema.json @@ -1 +1,14 @@ -{"$schema":"https://json-schema.org/draft/2020-12/schema","type":"object","additionalProperties":false,"required":["tasks"],"properties":{"tasks":{"type":"array","items":{"$ref":"https://aifa.dev/task-management/contracts/v1/task-view.schema.json"}}}} +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "title": "ListTasksOutputV1", + "type": "object", + "additionalProperties": false, + "required": ["tasks"], + "properties": { + "tasks": { + "type": "array", + "items": { "$ref": "https://aifa.dev/task-management/contracts/v1/task-view.schema.json" } + }, + "nextCursor": { "type": "string" } + } +} diff --git a/demo/ai-todo-assistant/contexts/task-management/features/list-tasks/feature.definition.json b/demo/ai-todo-assistant/contexts/task-management/features/list-tasks/feature.definition.json index 6184ba5..fd1c50d 100644 --- a/demo/ai-todo-assistant/contexts/task-management/features/list-tasks/feature.definition.json +++ b/demo/ai-todo-assistant/contexts/task-management/features/list-tasks/feature.definition.json @@ -4,15 +4,43 @@ "boundedContext": "task-management", "businessNeed": "Give people and connected AI clients a single filterable source of truth for commitments across life categories.", "dependsOn": [], - "consumesContracts": [], - "backend": { "route": "/api/tasks", "method": "GET", "capabilities": ["TaskList"], "mcp": { "exposed": true, "toolName": "list_tasks" } }, - "frontend": { "slots": ["TaskList", "TaskSummary"] }, - "contracts": { "input": "contracts/input.schema.json", "output": "contracts/output.schema.json", "mcpInput": "contracts/input.schema.json" }, + "consumesContracts": ["task-management/contracts/v1/task-cache.schema.json"], + "backend": { + "route": "/api/tasks", + "method": "GET", + "capabilities": ["TaskList"], + "mcp": { "exposed": true, "toolName": "list_tasks" } + }, + "frontend": { "slots": ["TaskList", "AppHeader", "AppNavigation", "AppContent", "AppFooter"] }, + "contracts": { + "input": "contracts/input.schema.json", + "output": "contracts/output.schema.json", + "mcpInput": "contracts/input.schema.json" + }, "events": [], - "security": { "mutation": false, "actorRequired": true, "idempotent": true, "requiresConfirmation": false, "concurrency": "none", "mcpScopes": ["TaskRead"] }, + "security": { + "mutation": false, + "actorRequired": true, + "idempotent": true, + "requiresConfirmation": false, + "concurrency": "none", + "mcpScopes": ["TaskRead"] + }, "workItems": [ - { "id": "LST-1", "description": "Define category and status filtering with enum validation.", "acceptanceCriterion": "Filters accept only TaskCategory and TaskStatus values." }, - { "id": "LST-2", "description": "Implement the read-only AIFA feature and Mongo capability query.", "acceptanceCriterion": "Results are deterministically ordered." }, - { "id": "LST-3", "description": "Register MUI list and summary slot contributions.", "acceptanceCriterion": "The empty, filtered, and populated states render correctly." } + { + "id": "LST-1", + "description": "Define category and status filtering with enum validation.", + "acceptanceCriterion": "Filters accept only TaskCategory and TaskStatus values." + }, + { + "id": "LST-2", + "description": "Implement the read-only AIFA feature and Mongo capability query.", + "acceptanceCriterion": "Results are deterministically ordered." + }, + { + "id": "LST-3", + "description": "Register MUI list and summary slot contributions.", + "acceptanceCriterion": "The empty, filtered, and populated states render correctly." + } ] } diff --git a/demo/ai-todo-assistant/contexts/task-management/features/list-tasks/frontend/contribution.tsx b/demo/ai-todo-assistant/contexts/task-management/features/list-tasks/frontend/contribution.tsx new file mode 100644 index 0000000..cdfb5ff --- /dev/null +++ b/demo/ai-todo-assistant/contexts/task-management/features/list-tasks/frontend/contribution.tsx @@ -0,0 +1,1295 @@ +import AddRounded from "@mui/icons-material/AddRounded"; +import ArrowForwardRounded from "@mui/icons-material/ArrowForwardRounded"; +import AutoAwesomeRounded from "@mui/icons-material/AutoAwesomeRounded"; +import BoltRounded from "@mui/icons-material/BoltRounded"; +import CalendarMonthRounded from "@mui/icons-material/CalendarMonthRounded"; +import CheckCircleRounded from "@mui/icons-material/CheckCircleRounded"; +import DashboardRounded from "@mui/icons-material/DashboardRounded"; +import GridViewRounded from "@mui/icons-material/GridViewRounded"; +import KeyboardArrowDownRounded from "@mui/icons-material/KeyboardArrowDownRounded"; +import TaskAltRounded from "@mui/icons-material/TaskAltRounded"; +import TimelapseRounded from "@mui/icons-material/TimelapseRounded"; +import TrendingUpRounded from "@mui/icons-material/TrendingUpRounded"; +import { + Alert, + Avatar, + Box, + Button, + Chip, + IconButton, + LinearProgress, + List, + ListItemButton, + ListItemIcon, + ListItemText, + Paper, + Stack, + Typography, +} from "@mui/material"; +import type { ReactNode } from "react"; +import { + TaskCategory, + TaskPriority, + TaskStatus, +} from "../../../contracts/v1/task-taxonomy.js"; +import type { + ShellModel, + TaskListModel, + TaskView, +} from "../../../../../core/frontend/app-model.js"; +import type { + AnySlotContribution, + SlotContribution, +} from "../../../../../core/frontend/slots/slot-registry.js"; +import { AppSurface, SlotName } from "../../../../../core/shared/architecture-enums.js"; +import { Slot } from "../../../../../core/frontend/slots/Slot.js"; +import { TaskWorkspaceProvider, useTaskWorkspace } from "./controller.js"; +import { visualTokens } from "../../../../../core/frontend/theme.js"; + +type TaskWorkspaceViewModel = ShellModel & TaskListModel; + +function WorkspaceError({ message }: { message: string }) { + return message ? {message} : null; +} + +const statusColumns = [ + { status: TaskStatus.Todo, label: "To do", color: "#6c7480", tint: "#f0f1f3" }, + { status: TaskStatus.InProgress, label: "In progress", color: visualTokens.purple, tint: visualTokens.purpleTint }, + { status: TaskStatus.Completed, label: "Completed", color: visualTokens.green, tint: visualTokens.greenTint }, +] as const; + +const categoryColors: Record = { + Work: { background: visualTokens.purpleTint, color: visualTokens.purple }, + Personal: { background: visualTokens.orangeTint, color: "#b85d1d" }, + Sport: { background: visualTokens.sportTint, color: "#13775a" }, + Shopping: { background: visualTokens.shoppingTint, color: "#9b3fa0" }, + Other: { background: "#edf0f3", color: "#5f6875" }, +}; + +function Brand({ inverse = false, compact = false }: { inverse?: boolean; compact?: boolean }) { + return ( + + + + + + focusly + + + ); +} + +function AppHeader({ model }: { model: ShellModel }) { + if (model.activeSurface === AppSurface.Landing) { + return ( + + + + {[ + ["Product", "product"], + ["How it works", "how-it-works"], + ["Why Focusly", "why-focusly"], + ].map(([label, target]) => ( + + ))} + + + + + + ); + } + return ( + + + + My workspace + + + {model.activeSurface === AppSurface.Settings + ? "AI connections" + : model.activeSurface === AppSurface.Tasks + ? "My tasks" + : model.activeSurface === AppSurface.Planner + ? "AI planner" + : "Task overview"} + + + + + + + AI assistant ready + + + + AW + + + + + + ); +} + +function MiniTask({ + title, + category, + accent, +}: { + title: string; + category: string; + accent: string; +}) { + return ( + + + {title} + + + + + {category} + + + AW + + + ); +} + +function ProductPreview() { + const columns = [ + { + label: "To do", + count: 3, + items: [ + ["Build launch checklist", "Work", "#eeecff"], + ["Book Friday dinner", "Personal", "#fff0e6"], + ], + }, + { + label: "In progress", + count: 2, + items: [ + ["Shape product narrative", "Work", "#d9ff57"], + ["Morning run · 5 km", "Sport", "#e5f7f0"], + ], + }, + { label: "Completed", count: 8, items: [["Review weekly goals", "Personal", "#e4f6ef"]] }, + ]; + return ( + + + + {["#ff6b6b", "#ffd43b", "#51cf66"].map((color) => ( + + ))} + + + + + + + + {["Overview", "My tasks", "AI planner"].map((item, index) => ( + + + {item} + + + ))} + + + + + + + GOOD MORNING, ADRIAN + + + Make today count. + + + + + + {columns.map((column) => ( + + + + {column.label} + + + {column.count} + + + + {column.items.map(([title, category, accent]) => ( + + ))} + + + ))} + + + + + + + + AI plan ready + + + + + ); +} + +function LandingPage({ model }: { model: ShellModel }) { + if (model.activeSurface !== AppSurface.Landing) return null; + return ( + + + + + + + + + A calmer way to get things done + + + + Your day, finally +
      + in{" "} + + clear focus. + +
      + + Turn scattered thoughts into an actionable plan. Focusly combines a beautiful task + workspace with an AI copilot that helps you find the next right move. + + + + + + + {["No credit card", "Set up in 30 seconds", "Local AI ready"].map((text) => ( + + + + {text} + + + ))} + +
      + +
      +
      + + + + + + DESIGNED FOR MOMENTUM + + + Less organizing. +
      + More moving. +
      +
      + + Every screen is built to reduce friction—from capturing a thought to finishing the + work that matters. + +
      + + {[ + { + n: "01", + icon: , + title: "Describe the outcome", + text: "Tell the AI what you want to achieve in plain language.", + }, + { + n: "02", + icon: , + title: "Shape the plan", + text: "Review categorized suggestions and add the useful ones in a click.", + }, + { + n: "03", + icon: , + title: "Keep momentum", + text: "Move work across a clear board and see progress build naturally.", + }, + ].map((item, index) => ( + + + + {item.icon} + + + {item.n} + + + + {item.title} + + + {item.text} + + + ))} + +
      +
      + + + + + Make room for the work that matters. + + + + +
      + ); +} + +function TaskCard({ model, task }: { model: TaskListModel; task: TaskView }) { + const categoryColor = categoryColors[task.category] ?? categoryColors.Other; + return ( + + + + + {task.title} + + + + + + + {task.priority} priority + + + + + + + + ); +} + +function TaskList({ model }: { model: TaskListModel }) { + const visibleTasks = model.categoryFilter + ? model.tasks.filter((task) => task.category === model.categoryFilter) + : model.tasks; + return ( + + + + + Your task flow + + + Move with clarity, one task at a time. + + + + model.setCategoryFilter("")} + sx={{ flexShrink: 0, height: 28, "& .MuiChip-label": { px: 1.25 } }} + /> + {Object.values(TaskCategory).map((category) => ( + model.setCategoryFilter(category)} + sx={{ flexShrink: 0, height: 28, "& .MuiChip-label": { px: 1.25 } }} + /> + ))} + + + + {statusColumns.map((column) => { + const columnTasks = visibleTasks.filter((task) => task.status === column.status); + return ( + + + + + {column.label} + + + {columnTasks.length} + + + + + document + .getElementById("quick-capture") + ?.scrollIntoView({ behavior: "smooth", block: "center" }) + } + > + + + + + {columnTasks.length ? ( + columnTasks.map((task) => ) + ) : ( + + + + + This lane is clear + + + + )} + + + ); + })} + + + ); +} + +function Navigation({ model }: { model: ShellModel }) { + return ( + + + + + + WORKSPACE + + + model.setActiveSurface(AppSurface.Dashboard)} + sx={{ + borderRadius: 2, + py: 1.1, + color: "#aeb2bd", + "&.Mui-selected": { bgcolor: "rgba(255,255,255,.1)", color: "white" }, + "&.Mui-selected:hover": { bgcolor: "rgba(255,255,255,.13)" }, + }} + > + + + + + + model.setActiveSurface(AppSurface.Tasks)} + sx={{ + borderRadius: 2, + py: 1.1, + color: "#aeb2bd", + "&.Mui-selected": { bgcolor: "rgba(255,255,255,.1)", color: "white" }, + "&.Mui-selected:hover": { bgcolor: "rgba(255,255,255,.13)" }, + }} + > + + + + + + model.setActiveSurface(AppSurface.Planner)} + sx={{ + borderRadius: 2, + py: 1.1, + color: "#aeb2bd", + "&.Mui-selected": { bgcolor: "rgba(255,255,255,.1)", color: "white" }, + "&.Mui-selected:hover": { bgcolor: "rgba(255,255,255,.13)" }, + }} + > + + + + + + + + + + + + + + + Weekly momentum + + + Keep the streak alive + + + + + + + + ); +} + +function Metric({ + icon, + value, + label, + accent, +}: { + icon: ReactNode; + value: number; + label: string; + accent: string; +}) { + return ( + + + + {icon} + + + + {value} + + + {label} + + + + + ); +} + +function Dashboard({ model }: { model: TaskWorkspaceViewModel }) { + if (model.activeSurface !== AppSurface.Dashboard) return null; + const completed = model.tasks.filter((task) => task.status === TaskStatus.Completed).length; + const active = model.tasks.filter((task) => task.status === TaskStatus.InProgress).length; + const due = model.tasks.filter((task) => task.status === TaskStatus.Todo).length; + const today = new Intl.DateTimeFormat("en-US", { + weekday: "long", + month: "long", + day: "numeric", + }) + .format(new Date()) + .toUpperCase(); + return ( + + + + + + {today} + + + Make today count. + + + You’re closer than you think. Pick the next meaningful move. + + + + + + } + value={model.tasks.length} + label="Total tasks" + accent="#eeecff" + /> + } + value={active} + label="In progress" + accent="#fff0d7" + /> + } + value={completed} + label="Completed" + accent="#e4f6ef" + /> + } + value={due} + label="Ready to start" + accent="#f9eafa" + /> + + + + + + + + + + Quick capture + + + + + + ); +} + +function TasksPage({ model }: { model: TaskWorkspaceViewModel }) { + if (model.activeSurface !== AppSurface.Tasks) return null; + return ( + + + + + TASKS + + + Everything on your plate. + + + Capture, prioritize, and move work forward from one focused view. + + + + + + + + Add something new + + + + + + ); +} + +function PlannerPage({ model }: { model: TaskWorkspaceViewModel }) { + if (model.activeSurface !== AppSurface.Planner) return null; + return ( + + + + + AI PLANNER + + + Start with the outcome. + + + Describe where you want to go. Focusly will turn it into practical, reviewable steps + without adding anything until you approve it. + + + + + {[ + ["01", "Describe", "Share a goal in your own words."], + ["02", "Review", "Refine the categorized suggestions."], + ["03", "Act", "Add only the tasks that move you forward."], + ].map(([number, title, text]) => ( + + + {number} + + + {title} + + + {text} + + + ))} + + + ); +} + +function TaskWorkspaceContentInner({ shell }: { shell: ShellModel }) { + const model = { ...shell, ...useTaskWorkspace() }; + return ( + <> + + + + + ); +} +function TaskWorkspaceContent({ shell }: { shell: ShellModel }) { + if (shell.activeSurface === AppSurface.Landing) return ; + if (!shell.signedIn) return null; + return ( + + + + ); +} + +export const contribution: SlotContribution = { + slot: SlotName.TaskList, + name: "task-list", + render: (model) => , +}; + +export const contributions: AnySlotContribution[] = [ + { + slot: SlotName.AppHeader, + name: "workspace-header", + order: 10, + render: (model) => , + }, + { + slot: SlotName.AppNavigation, + name: "task-workspace-navigation", + order: 10, + render: (model) => , + }, + { + slot: SlotName.AppContent, + name: "task-workspace-content", + order: 10, + render: (model) => , + }, + { + slot: SlotName.AppFooter, + name: "task-workspace-footer", + order: 10, + render: (model) => + model.activeSurface === AppSurface.Landing ? ( + + + + + © 2026 Focusly. Built on AIFA’s composable feature architecture. + + + + ) : null, + }, +]; diff --git a/demo/ai-todo-assistant/contexts/task-management/features/list-tasks/frontend/controller.tsx b/demo/ai-todo-assistant/contexts/task-management/features/list-tasks/frontend/controller.tsx new file mode 100644 index 0000000..33384bc --- /dev/null +++ b/demo/ai-todo-assistant/contexts/task-management/features/list-tasks/frontend/controller.tsx @@ -0,0 +1,39 @@ +import { + createContext, + useContext, + useMemo, + useState, + type ReactNode, +} from "react"; +import { + type TaskCategory, + type TaskListModel, + type TaskStatus, +} from "../../../../../core/frontend/app-model.js"; +import { useTasks } from "./use-tasks.js"; + +const TaskWorkspaceContext = createContext(null); + +export function TaskWorkspaceProvider({ children }: { children: ReactNode }) { + const [categoryFilter, setCategoryFilter] = useState(""); + const [statusFilter, setStatusFilter] = useState(""); + const query = useTasks({ category: categoryFilter, status: statusFilter }); + const model = useMemo( + () => ({ + tasks: query.data ?? [], + error: query.error?.message ?? "", + categoryFilter, + statusFilter, + setCategoryFilter, + setStatusFilter, + }), + [categoryFilter, query.data, query.error, statusFilter], + ); + return {children}; +} + +export function useTaskWorkspace(): TaskListModel { + const model = useContext(TaskWorkspaceContext); + if (!model) throw new Error("TaskWorkspaceProvider is not registered"); + return model; +} diff --git a/demo/ai-todo-assistant/contexts/task-management/features/list-tasks/frontend/use-tasks.ts b/demo/ai-todo-assistant/contexts/task-management/features/list-tasks/frontend/use-tasks.ts new file mode 100644 index 0000000..f4e188b --- /dev/null +++ b/demo/ai-todo-assistant/contexts/task-management/features/list-tasks/frontend/use-tasks.ts @@ -0,0 +1,38 @@ +import { useMemo } from "react"; +import { api } from "../../../../../core/frontend/api.js"; +import { useQuery } from "../../../../../core/frontend/query/react-query.js"; +import type { + TaskManagementTaxonomyV1, + TaskViewV1, +} from "../../../../../core/shared/generated/contracts.js"; +import { TaskCacheTag, TaskQueryName } from "../../../contracts/v1/task-cache.js"; + +type TaskCategory = TaskManagementTaxonomyV1["category"]; +type TaskStatus = TaskManagementTaxonomyV1["status"]; + +export interface TaskFilters { + category: TaskCategory | ""; + status: TaskStatus | ""; +} + +export function useTasks(filters: TaskFilters) { + const key = useMemo( + () => [TaskQueryName.List, filters.category, filters.status] as const, + [filters.category, filters.status], + ); + return useQuery({ + key, + tags: [TaskCacheTag.Collection], + queryFn: async ({ signal }) => { + const query = new URLSearchParams({ + ...(filters.category ? { category: filters.category } : {}), + ...(filters.status ? { status: filters.status } : {}), + }); + return ( + await api<{ tasks: TaskViewV1[] }>(`/api/tasks${query.size ? `?${query}` : ""}`, { + signal, + }) + ).tasks; + }, + }); +} diff --git a/demo/ai-todo-assistant/contexts/task-management/features/list-tasks/manifest.ts b/demo/ai-todo-assistant/contexts/task-management/features/list-tasks/manifest.ts new file mode 100644 index 0000000..328f1ec --- /dev/null +++ b/demo/ai-todo-assistant/contexts/task-management/features/list-tasks/manifest.ts @@ -0,0 +1,26 @@ +import { + HttpMethod, + PermissionScope, + SlotName, +} from "../../../../core/shared/architecture-enums.js"; +import { feature } from "./backend/feature.js"; +import type { FeatureManifest } from "../../../../core/shared/feature-manifest.js"; +export const manifest = { + name: feature.name, + backend: { + method: HttpMethod.Get, + route: "/api/tasks", + feature, + requiredScopes: [PermissionScope.TaskRead], + }, + frontend: { + contributions: [ + { slot: SlotName.TaskList, name: "task-list" }, + { slot: SlotName.AppHeader, name: "workspace-header" }, + { slot: SlotName.AppNavigation, name: "task-workspace-navigation" }, + { slot: SlotName.AppContent, name: "task-workspace-content" }, + { slot: SlotName.AppFooter, name: "task-workspace-footer" }, + ], + }, + mcp: { toolName: "list_tasks" }, +} satisfies FeatureManifest; diff --git a/demo/ai-todo-assistant/contexts/task-management/infrastructure/manifest.ts b/demo/ai-todo-assistant/contexts/task-management/infrastructure/manifest.ts new file mode 100644 index 0000000..f7540de --- /dev/null +++ b/demo/ai-todo-assistant/contexts/task-management/infrastructure/manifest.ts @@ -0,0 +1,10 @@ +import type { ContextInfrastructureManifest } from "../../../core/backend/discovery/context-infrastructure.js"; +import { createMongoTaskCapabilities, ensureTaskManagementIndexes } from "./mongo-task-capabilities.js"; + +export const infrastructureManifest = { + name: "task-management", + ensureIndexes: ensureTaskManagementIndexes, + createCapabilities({ database, actor, session }) { + return { ...createMongoTaskCapabilities(database, actor, session) }; + }, +} satisfies ContextInfrastructureManifest; diff --git a/demo/ai-todo-assistant/contexts/task-management/infrastructure/mongo-task-capabilities.ts b/demo/ai-todo-assistant/contexts/task-management/infrastructure/mongo-task-capabilities.ts new file mode 100644 index 0000000..8b82845 --- /dev/null +++ b/demo/ai-todo-assistant/contexts/task-management/infrastructure/mongo-task-capabilities.ts @@ -0,0 +1,93 @@ +import type { ClientSession, Db } from "mongodb"; +import type { Actor } from "../../../core/shared/aifa.js"; +import type { Task } from "../domain/task.js"; +import type { TaskPersistenceCapabilities } from "../domain/task-capabilities.js"; + +function withoutMongoId(document: Task & { _id?: unknown }): Task { + const { _id: _mongoId, ...task } = document; + return task; +} + +export function createMongoTaskCapabilities( + database: Db, + actor: Actor, + session?: ClientSession, +): TaskPersistenceCapabilities { + const tasks = database.collection("tasks"); + return { + async TaskCreate({ task }) { + const tenantTask: Task = { ...task, tenantId: actor.tenantId }; + await tasks.insertOne(tenantTask, { session }); + return withoutMongoId(tenantTask); + }, + async TaskList({ category, status, cursor, limit }) { + const page = await tasks + .find( + { + tenantId: actor.tenantId, + ...(category ? { category } : {}), + ...(status ? { status } : {}), + ...(cursor + ? { + $or: [ + { createdAt: { $lt: cursor.createdAt } }, + { createdAt: cursor.createdAt, id: { $lt: cursor.id } }, + ], + } + : {}), + }, + { session }, + ) + .sort({ createdAt: -1, id: -1 }) + .limit(limit + 1) + .toArray(); + const hasNextPage = page.length > limit; + const selected = hasNextPage ? page.slice(0, limit) : page; + const last = selected.at(-1); + return { + tasks: selected.map(withoutMongoId), + ...(hasNextPage && last + ? { + nextCursor: Buffer.from( + JSON.stringify({ createdAt: last.createdAt, id: last.id }), + ).toString("base64url"), + } + : {}), + }; + }, + async TaskLoad({ taskId }) { + const task = await tasks.findOne({ tenantId: actor.tenantId, id: taskId }, { session }); + return task ? withoutMongoId(task) : null; + }, + async TaskSave({ taskId, expectedVersion, changes }) { + const task = await tasks.findOneAndUpdate( + { tenantId: actor.tenantId, id: taskId, version: expectedVersion }, + { $set: changes }, + { returnDocument: "after", session }, + ); + return task ? withoutMongoId(task) : null; + }, + async TaskDelete({ taskId, expectedVersion }) { + return ( + (await tasks.deleteOne( + { tenantId: actor.tenantId, id: taskId, version: expectedVersion }, + { session }, + )).deletedCount === 1 + ); + }, + }; +} + +export async function ensureTaskManagementIndexes(database: Db): Promise { + await Promise.all([ + database + .collection("tasks") + .createIndex({ tenantId: 1, id: 1 }, { unique: true, name: "tenant_task_id" }), + database + .collection("tasks") + .createIndex( + { tenantId: 1, category: 1, status: 1, createdAt: -1, id: -1 }, + { name: "tenant_task_filters" }, + ), + ]); +} diff --git a/demo/ai-todo-assistant/contexts/workspace-settings/features/manage-ai-settings/IMPLEMENTATION_PLAN.md b/demo/ai-todo-assistant/contexts/workspace-settings/features/manage-ai-settings/IMPLEMENTATION_PLAN.md new file mode 100644 index 0000000..60e1944 --- /dev/null +++ b/demo/ai-todo-assistant/contexts/workspace-settings/features/manage-ai-settings/IMPLEMENTATION_PLAN.md @@ -0,0 +1,4 @@ +# Manage AI Settings + +This feature owns the connection-preference UI and the future persistence boundary. +It must not import AI Planning feature code or provider infrastructure. diff --git a/demo/ai-todo-assistant/contexts/workspace-settings/features/manage-ai-settings/backend/feature.ts b/demo/ai-todo-assistant/contexts/workspace-settings/features/manage-ai-settings/backend/feature.ts new file mode 100644 index 0000000..2790a67 --- /dev/null +++ b/demo/ai-todo-assistant/contexts/workspace-settings/features/manage-ai-settings/backend/feature.ts @@ -0,0 +1,18 @@ +import { FeatureName } from "../../../../../core/shared/generated/feature-names.js"; +import type { AifaFeature } from "../../../../../core/shared/aifa.js"; + +/** + * Provider configuration is runtime-managed. This read boundary deliberately does + * not expose endpoints or credentials to the browser. + */ +export const feature: AifaFeature< + Record, + { configured: boolean }, + Record +> = { + name: FeatureName.ManageAiSettings, + capabilities: [], + async execute(context) { + return context.ok({ configured: true }); + }, +}; diff --git a/demo/ai-todo-assistant/contexts/workspace-settings/features/manage-ai-settings/contracts/input.schema.json b/demo/ai-todo-assistant/contexts/workspace-settings/features/manage-ai-settings/contracts/input.schema.json new file mode 100644 index 0000000..22e3bac --- /dev/null +++ b/demo/ai-todo-assistant/contexts/workspace-settings/features/manage-ai-settings/contracts/input.schema.json @@ -0,0 +1,6 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "title": "ManageAiSettingsInputV1", + "type": "object", + "additionalProperties": false +} diff --git a/demo/ai-todo-assistant/contexts/workspace-settings/features/manage-ai-settings/contracts/output.schema.json b/demo/ai-todo-assistant/contexts/workspace-settings/features/manage-ai-settings/contracts/output.schema.json new file mode 100644 index 0000000..32f4716 --- /dev/null +++ b/demo/ai-todo-assistant/contexts/workspace-settings/features/manage-ai-settings/contracts/output.schema.json @@ -0,0 +1,8 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "title": "ManageAiSettingsOutputV1", + "type": "object", + "additionalProperties": false, + "required": ["configured"], + "properties": { "configured": { "type": "boolean" } } +} diff --git a/demo/ai-todo-assistant/contexts/workspace-settings/features/manage-ai-settings/feature.definition.json b/demo/ai-todo-assistant/contexts/workspace-settings/features/manage-ai-settings/feature.definition.json new file mode 100644 index 0000000..7a6a962 --- /dev/null +++ b/demo/ai-todo-assistant/contexts/workspace-settings/features/manage-ai-settings/feature.definition.json @@ -0,0 +1,37 @@ +{ + "$schema": "../../../../core/architecture/feature-definition.schema.json", + "name": "manage-ai-settings", + "boundedContext": "workspace-settings", + "businessNeed": "Let people choose their preferred AI connection independently from task-planning workflows and providers.", + "dependsOn": [], + "consumesContracts": [], + "backend": { + "route": "/api/settings/ai", + "method": "GET", + "capabilities": [], + "mcp": { "exposed": false } + }, + "frontend": { "slots": ["AppNavigation", "AppContent", "SettingsPanel"] }, + "contracts": { "input": "contracts/input.schema.json", "output": "contracts/output.schema.json" }, + "events": [], + "security": { + "mutation": false, + "actorRequired": true, + "idempotent": true, + "requiresConfirmation": false, + "concurrency": "none", + "mcpScopes": ["SettingsRead"] + }, + "workItems": [ + { + "id": "SET-1", + "description": "Provide an independently discoverable AI settings surface.", + "acceptanceCriterion": "The settings UI is not imported by AI Planning or Task Management." + }, + { + "id": "SET-2", + "description": "Reserve a governed settings feature boundary for persistence.", + "acceptanceCriterion": "Future settings storage can be added through declared capabilities without changing planning features." + } + ] +} diff --git a/demo/ai-todo-assistant/contexts/workspace-settings/features/manage-ai-settings/frontend/contribution.tsx b/demo/ai-todo-assistant/contexts/workspace-settings/features/manage-ai-settings/frontend/contribution.tsx new file mode 100644 index 0000000..7bc4bff --- /dev/null +++ b/demo/ai-todo-assistant/contexts/workspace-settings/features/manage-ai-settings/frontend/contribution.tsx @@ -0,0 +1,134 @@ +import CheckCircleRounded from "@mui/icons-material/CheckCircleRounded"; +import HubRounded from "@mui/icons-material/HubRounded"; +import SettingsRounded from "@mui/icons-material/SettingsRounded"; +import SmartToyRounded from "@mui/icons-material/SmartToyRounded"; +import { + Card, + CardContent, + Divider, + List, + ListItemButton, + ListItemIcon, + ListItemText, + Stack, + Typography, +} from "@mui/material"; +import type { ShellModel } from "../../../../../core/frontend/app-model.js"; +import { Slot } from "../../../../../core/frontend/slots/Slot.js"; +import type { + AnySlotContribution, + SlotContribution, +} from "../../../../../core/frontend/slots/slot-registry.js"; +import { AppSurface, SlotName } from "../../../../../core/shared/architecture-enums.js"; + +function Settings({ model: _model }: { model: ShellModel }) { + return ( + + + + + + + + + + + Browser-side provider URLs and credentials are intentionally disabled. Configure + OLLAMA_BASE_URL and OLLAMA_MODEL on the API runtime, then use readiness checks and + server telemetry to verify the connection. + + + + + + + + + + Connection status + + + + + Provider settings are enforced by the runtime + + + + Provider credentials and connection endpoints never enter browser storage. + + + + + + ); +} +function BoxTitle({ title, description }: { title: string; description: string }) { + return ( + + + {title} + + + {description} + + + ); +} +function Navigation({ model }: { model: ShellModel }) { + return ( + + model.setActiveSurface(AppSurface.Settings)} + sx={{ + borderRadius: 2, + py: 1.1, + color: "#aeb2bd", + "&.Mui-selected": { bgcolor: "rgba(255,255,255,.1)", color: "white" }, + "&.Mui-selected:hover": { bgcolor: "rgba(255,255,255,.13)" }, + }} + > + + + + + + + ); +} +function SettingsPage({ model }: { model: ShellModel }) { + if (model.activeSurface !== AppSurface.Settings) return null; + return ( + + + + + ); +} + +export const contribution: SlotContribution = { + slot: SlotName.SettingsPanel, + name: "ai-settings-panel", + order: 10, + render: (model) => , +}; +export const contributions: AnySlotContribution[] = [ + { + slot: SlotName.AppNavigation, + name: "ai-settings-navigation", + order: 20, + render: (model) => , + }, + { + slot: SlotName.AppContent, + name: "ai-settings-page", + order: 20, + render: (model) => , + }, +]; diff --git a/demo/ai-todo-assistant/contexts/workspace-settings/features/manage-ai-settings/manifest.ts b/demo/ai-todo-assistant/contexts/workspace-settings/features/manage-ai-settings/manifest.ts new file mode 100644 index 0000000..80432ff --- /dev/null +++ b/demo/ai-todo-assistant/contexts/workspace-settings/features/manage-ai-settings/manifest.ts @@ -0,0 +1,24 @@ +import { + HttpMethod, + PermissionScope, + SlotName, +} from "../../../../core/shared/architecture-enums.js"; +import { feature } from "./backend/feature.js"; +import type { FeatureManifest } from "../../../../core/shared/feature-manifest.js"; + +export const manifest = { + name: feature.name, + backend: { + method: HttpMethod.Get, + route: "/api/settings/ai", + feature, + requiredScopes: [PermissionScope.SettingsRead], + }, + frontend: { + contributions: [ + { slot: SlotName.AppNavigation, name: "ai-settings-navigation" }, + { slot: SlotName.AppContent, name: "ai-settings-page" }, + { slot: SlotName.SettingsPanel, name: "ai-settings-panel" }, + ], + }, +} satisfies FeatureManifest; diff --git a/demo/ai-todo-assistant/core/architecture/.dependency-cruiser.cjs b/demo/ai-todo-assistant/core/architecture/.dependency-cruiser.cjs index d9770b0..ebc0a87 100644 --- a/demo/ai-todo-assistant/core/architecture/.dependency-cruiser.cjs +++ b/demo/ai-todo-assistant/core/architecture/.dependency-cruiser.cjs @@ -1,12 +1,72 @@ +const { readdirSync } = require("node:fs"); +const { join } = require("node:path"); + +const contextsRoot = join(__dirname, "../../contexts"); +const contexts = readdirSync(contextsRoot, { withFileTypes: true }) + .filter((entry) => entry.isDirectory()) + .map((entry) => entry.name); +const contextIsolation = contexts.flatMap((source) => + contexts + .filter((target) => target !== source) + .map((target) => ({ + name: `${source}-cannot-import-${target}-implementation`, + severity: "error", + from: { path: `^contexts/${source}/` }, + to: { path: `^contexts/${target}/(domain|features|infrastructure)/` }, + })), +); +const featureIsolation = contexts.flatMap((context) => { + const root = join(contextsRoot, context, "features"); + let features = []; + try { + features = readdirSync(root, { withFileTypes: true }) + .filter((entry) => entry.isDirectory()) + .map((entry) => entry.name); + } catch {} + return features.flatMap((source) => + features + .filter((target) => target !== source) + .map((target) => ({ + name: `${context}-${source}-cannot-import-${target}`, + severity: "error", + from: { path: `^contexts/${context}/features/${source}/` }, + to: { path: `^contexts/${context}/features/${target}/` }, + })), + ); +}); + module.exports = { forbidden: [ { name: "no-circular-imports", severity: "error", from: {}, to: { circular: true } }, - { name: "core-cannot-import-contexts", severity: "error", from: { path: "^core/" }, to: { path: "^contexts/" } }, - { name: "task-management-cannot-import-ai-planning-implementation", severity: "error", from: { path: "^contexts/task-management/(domain|features)/" }, to: { path: "^contexts/ai-planning/(domain|features)/" } }, - { name: "ai-planning-cannot-import-task-management-implementation", severity: "error", from: { path: "^contexts/ai-planning/(domain|features)/" }, to: { path: "^contexts/task-management/(domain|features)/" } } + { + name: "core-cannot-import-contexts", + severity: "error", + from: { path: "^core/" }, + to: { path: "^contexts/" }, + }, + { + name: "features-cannot-import-infrastructure", + severity: "error", + from: { path: "^contexts/[^/]+/features/" }, + to: { path: "^contexts/[^/]+/infrastructure/" }, + }, + { + name: "domain-cannot-import-features-or-infrastructure", + severity: "error", + from: { path: "^contexts/[^/]+/domain/" }, + to: { path: "^contexts/[^/]+/(features|infrastructure)/" }, + }, + { + name: "infrastructure-cannot-import-features", + severity: "error", + from: { path: "^contexts/[^/]+/infrastructure/" }, + to: { path: "^contexts/[^/]+/features/" }, + }, + ...contextIsolation, + ...featureIsolation, ], options: { doNotFollow: { path: "node_modules" }, - tsConfig: { fileName: "tsconfig.json" } - } + tsConfig: { fileName: "tsconfig.json" }, + }, }; diff --git a/demo/ai-todo-assistant/core/architecture/check-bundle-size.mjs b/demo/ai-todo-assistant/core/architecture/check-bundle-size.mjs new file mode 100644 index 0000000..cf999a3 --- /dev/null +++ b/demo/ai-todo-assistant/core/architecture/check-bundle-size.mjs @@ -0,0 +1,14 @@ +import { readdir, stat } from "node:fs/promises"; +import { join } from "node:path"; + +const assets = join(process.cwd(), "dist/web/assets"); +const files = (await readdir(assets)).filter((file) => file.endsWith(".js")); +const sizes = await Promise.all(files.map(async (file) => ({ file, bytes: (await stat(join(assets, file))).size }))); +const largest = sizes.sort((left, right) => right.bytes - left.bytes)[0]; +const total = sizes.reduce((sum, entry) => sum + entry.bytes, 0); +if (!largest) throw new Error("No JavaScript bundle artifacts were found"); +if (largest.bytes > 400_000) + throw new Error(`Largest JavaScript chunk ${largest.file} is ${largest.bytes} bytes; budget is 400000`); +if (total > 600_000) + throw new Error(`Total JavaScript is ${total} bytes; budget is 600000`); +console.log(`Bundle budget passed: largest=${largest.bytes} bytes, total=${total} bytes.`); diff --git a/demo/ai-todo-assistant/core/architecture/copy-server-assets.mjs b/demo/ai-todo-assistant/core/architecture/copy-server-assets.mjs new file mode 100644 index 0000000..ac3c215 --- /dev/null +++ b/demo/ai-todo-assistant/core/architecture/copy-server-assets.mjs @@ -0,0 +1,21 @@ +import { mkdir, readdir, copyFile } from "node:fs/promises"; +import { dirname, join, relative, resolve } from "node:path"; +import { fileURLToPath } from "node:url"; + +const appRoot = resolve(dirname(fileURLToPath(import.meta.url)), "../.."); +const sourceRoot = join(appRoot, "contexts"); +const outputRoot = join(appRoot, "dist/server/contexts"); + +async function copyJson(directory) { + for (const entry of await readdir(directory, { withFileTypes: true })) { + const source = join(directory, entry.name); + if (entry.isDirectory()) await copyJson(source); + else if (entry.name.endsWith(".json")) { + const destination = join(outputRoot, relative(sourceRoot, source)); + await mkdir(dirname(destination), { recursive: true }); + await copyFile(source, destination); + } + } +} + +await copyJson(sourceRoot); diff --git a/demo/ai-todo-assistant/core/architecture/feature-definition.schema.json b/demo/ai-todo-assistant/core/architecture/feature-definition.schema.json index 934d480..8b5ec37 100644 --- a/demo/ai-todo-assistant/core/architecture/feature-definition.schema.json +++ b/demo/ai-todo-assistant/core/architecture/feature-definition.schema.json @@ -4,14 +4,37 @@ "title": "AIFA Feature Definition", "type": "object", "additionalProperties": false, - "required": ["name", "boundedContext", "businessNeed", "dependsOn", "consumesContracts", "backend", "frontend", "contracts", "events", "security", "workItems"], + "required": [ + "name", + "boundedContext", + "businessNeed", + "dependsOn", + "consumesContracts", + "backend", + "frontend", + "contracts", + "events", + "security", + "workItems" + ], "properties": { "$schema": { "type": "string" }, "name": { "type": "string", "pattern": "^[a-z]+(?:-[a-z]+)*$" }, "boundedContext": { "type": "string", "pattern": "^[a-z]+(?:-[a-z]+)*$" }, "businessNeed": { "type": "string", "minLength": 20 }, - "dependsOn": { "type": "array", "uniqueItems": true, "items": { "type": "string", "pattern": "^[a-z]+(?:-[a-z]+)*$" } }, - "consumesContracts": { "type": "array", "uniqueItems": true, "items": { "type": "string", "pattern": "^[a-z-]+/contracts/v[0-9]+/.+\\.schema\\.json$" } }, + "dependsOn": { + "type": "array", + "uniqueItems": true, + "items": { "type": "string", "pattern": "^[a-z]+(?:-[a-z]+)*$" } + }, + "consumesContracts": { + "type": "array", + "uniqueItems": true, + "items": { + "type": "string", + "pattern": "^[a-z-]+/contracts/(?:v[0-9]+|events/v[0-9]+)/.+\\.schema\\.json$" + } + }, "backend": { "type": "object", "additionalProperties": false, @@ -32,7 +55,26 @@ "type": "object", "additionalProperties": false, "required": ["slots"], - "properties": { "slots": { "type": "array", "items": { "type": "string" } } } + "properties": { + "slots": { "type": "array", "items": { "type": "string" } }, + "eventConsumers": { + "type": "array", + "uniqueItems": true, + "items": { + "type": "object", + "additionalProperties": false, + "required": ["name", "eventType", "contract"], + "properties": { + "name": { "type": "string", "pattern": "^[a-z]+(?:-[a-z]+)*$" }, + "eventType": { "type": "string" }, + "contract": { + "type": "string", + "pattern": "^[a-z-]+/contracts/events/v[0-9]+/.+\\.schema\\.json$" + } + } + } + } + } }, "contracts": { "type": "object", @@ -47,7 +89,14 @@ "security": { "type": "object", "additionalProperties": false, - "required": ["mutation", "actorRequired", "idempotent", "requiresConfirmation", "mcpScopes", "concurrency"], + "required": [ + "mutation", + "actorRequired", + "idempotent", + "requiresConfirmation", + "mcpScopes", + "concurrency" + ], "properties": { "mutation": { "type": "boolean" }, "actorRequired": { "type": "boolean" }, @@ -57,7 +106,14 @@ "mcpScopes": { "type": "array", "uniqueItems": true, "items": { "type": "string" } } } }, - "events": { "type": "array", "uniqueItems": true, "items": { "type": "string", "pattern": "^[a-z-]+/contracts/events/v[0-9]+/.+\\.schema\\.json$" } }, + "events": { + "type": "array", + "uniqueItems": true, + "items": { + "type": "string", + "pattern": "^[a-z-]+/contracts/events/v[0-9]+/.+\\.schema\\.json$" + } + }, "workItems": { "type": "array", "minItems": 1, @@ -75,8 +131,38 @@ }, "allOf": [ { - "if": { "type": "object", "properties": { "backend": { "type": "object", "properties": { "mcp": { "type": "object", "properties": { "exposed": { "const": true } }, "required": ["exposed"] } }, "required": ["mcp"] } }, "required": ["backend"] }, - "then": { "type": "object", "properties": { "backend": { "type": "object", "properties": { "mcp": { "type": "object", "properties": { "toolName": { "type": "string" } }, "required": ["toolName"] } } } } } + "if": { + "type": "object", + "properties": { + "backend": { + "type": "object", + "properties": { + "mcp": { + "type": "object", + "properties": { "exposed": { "const": true } }, + "required": ["exposed"] + } + }, + "required": ["mcp"] + } + }, + "required": ["backend"] + }, + "then": { + "type": "object", + "properties": { + "backend": { + "type": "object", + "properties": { + "mcp": { + "type": "object", + "properties": { "toolName": { "type": "string" } }, + "required": ["toolName"] + } + } + } + } + } } ] } diff --git a/demo/ai-todo-assistant/core/architecture/generate-contract-types.ts b/demo/ai-todo-assistant/core/architecture/generate-contract-types.ts index 47b279f..1db0f66 100644 --- a/demo/ai-todo-assistant/core/architecture/generate-contract-types.ts +++ b/demo/ai-todo-assistant/core/architecture/generate-contract-types.ts @@ -3,40 +3,143 @@ import { dirname, join, relative, resolve } from "node:path"; import { fileURLToPath } from "node:url"; import { compile } from "json-schema-to-typescript"; +import ts from "typescript"; const appRoot = resolve(dirname(fileURLToPath(import.meta.url)), "../.."); const contextsRoot = join(appRoot, "contexts"); const outputFile = join(appRoot, "core/shared/generated/contracts.ts"); +const featureNamesFile = join(appRoot, "core/shared/generated/feature-names.ts"); +const frontendModulesFile = join(appRoot, "composition/generated/frontend-modules.ts"); async function findSchemaFiles(directory: string): Promise { const entries = await readdir(directory, { withFileTypes: true }); - const files = await Promise.all(entries.map(async (entry) => { - const location = join(directory, entry.name); - if (entry.isDirectory()) return findSchemaFiles(location); - return location.endsWith(".schema.json") ? [location] : []; - })); + const files = await Promise.all( + entries.map(async (entry) => { + const location = join(directory, entry.name); + if (entry.isDirectory()) return findSchemaFiles(location); + return location.endsWith(".schema.json") ? [location] : []; + }), + ); return files.flat(); } async function main(): Promise { const files = (await findSchemaFiles(contextsRoot)).sort(); - const schemas = await Promise.all(files.map(async (file) => ({ file, schema: JSON.parse(await readFile(file, "utf8")) as Record }))); - const schemasById = new Map(schemas.flatMap(({ schema }) => typeof schema.$id === "string" ? [[schema.$id, schema] as const] : [])); + const schemas = await Promise.all( + files.map(async (file) => ({ + file, + schema: JSON.parse(await readFile(file, "utf8")) as Record, + })), + ); + const schemasById = new Map( + schemas.flatMap(({ schema }) => + typeof schema.$id === "string" ? [[schema.$id, schema] as const] : [], + ), + ); const expandReferences = (value: unknown, seen = new Set()): unknown => { if (Array.isArray(value)) return value.map((item) => expandReferences(item, seen)); if (!value || typeof value !== "object") return value; const object = value as Record; - if (typeof object.$ref === "string" && Object.keys(object).length === 1 && schemasById.has(object.$ref)) { + if ( + typeof object.$ref === "string" && + Object.keys(object).length === 1 && + schemasById.has(object.$ref) + ) { if (seen.has(object.$ref)) return object; return expandReferences(schemasById.get(object.$ref), new Set([...seen, object.$ref])); } - return Object.fromEntries(Object.entries(object).map(([key, item]) => [key, expandReferences(item, seen)])); + return Object.fromEntries( + Object.entries(object).map(([key, item]) => [key, expandReferences(item, seen)]), + ); }; - const declarations = await Promise.all(schemas.map(({ file, schema }) => - compile(expandReferences(schema) as object, relative(contextsRoot, file).replaceAll(/[/.\-]/g, "_"), { bannerComment: "" }), - )); + const declarations = await Promise.all( + schemas.map(({ file, schema }) => + compile( + expandReferences(schema) as object, + relative(contextsRoot, file).replaceAll(/[/.\-]/g, "_"), + { bannerComment: "" }, + ), + ), + ); + const combined = ts.createSourceFile( + "contracts.ts", + declarations.join("\n"), + ts.ScriptTarget.Latest, + true, + ts.ScriptKind.TS, + ); + const seen = new Map(); + const printer = ts.createPrinter({ newLine: ts.NewLineKind.LineFeed }); + const uniqueDeclarations = combined.statements.flatMap((statement) => { + const named = + (ts.isInterfaceDeclaration(statement) || ts.isTypeAliasDeclaration(statement)) && + statement.name + ? statement.name.text + : undefined; + const rendered = printer.printNode(ts.EmitHint.Unspecified, statement, combined); + if (!named) return [rendered]; + const previous = seen.get(named); + if (!previous) { + seen.set(named, rendered); + return [rendered]; + } + if (previous.replaceAll(/\s/g, "") !== rendered.replaceAll(/\s/g, "")) + throw new Error(`Generated contract name '${named}' has conflicting declarations`); + return []; + }); await mkdir(dirname(outputFile), { recursive: true }); - await writeFile(outputFile, `/* Generated by npm run generate:contracts. Do not edit manually. */\n\n${declarations.join("\n")}`); + await writeFile( + outputFile, + `/* Generated by npm run generate:contracts. Do not edit manually. */\n\n${uniqueDeclarations.join("\n\n")}\n`, + ); + async function findDefinitions(directory: string): Promise { + const entries = await readdir(directory, { withFileTypes: true }); + return ( + await Promise.all( + entries.map(async (entry) => { + const location = join(directory, entry.name); + if (entry.isDirectory()) return findDefinitions(location); + return entry.name === "feature.definition.json" ? [location] : []; + }), + ) + ).flat(); + } + const definitionFiles = await findDefinitions(contextsRoot); + const featureNames = await Promise.all( + definitionFiles.sort().map(async (file) => { + const definition = JSON.parse(await readFile(file, "utf8")) as { name: string }; + const property = definition.name + .split("-") + .map((part) => part[0]?.toUpperCase() + part.slice(1)) + .join(""); + return ` ${property}: ${JSON.stringify(definition.name)},`; + }), + ); + await writeFile( + featureNamesFile, + `/* Generated from feature.definition.json files. Do not edit manually. */\nexport const FeatureName = {\n${featureNames.join("\n")}\n} as const;\nexport type FeatureName = (typeof FeatureName)[keyof typeof FeatureName];\n`, + ); + const frontendModules = await Promise.all( + definitionFiles.sort().map(async (file) => { + const definition = JSON.parse(await readFile(file, "utf8")) as { + name: string; + frontend: { + slots: string[]; + eventConsumers?: { name: string; eventType: string; contract: string }[]; + }; + }; + const modulePath = relative( + dirname(frontendModulesFile), + join(dirname(file), "frontend", "contribution.js"), + ).replaceAll("\\", "/"); + return ` {\n id: ${JSON.stringify(definition.name)},\n slots: ${JSON.stringify(definition.frontend.slots)} as const,\n eventConsumers: ${JSON.stringify(definition.frontend.eventConsumers ?? [])} as const,\n load: () => import(${JSON.stringify(modulePath.startsWith(".") ? modulePath : `./${modulePath}`)}),\n },`; + }), + ); + await mkdir(dirname(frontendModulesFile), { recursive: true }); + await writeFile( + frontendModulesFile, + `/* Generated from feature definitions. Do not edit manually. */\nexport const frontendModules = [\n${frontendModules.join("\n")}\n] as const;\n`, + ); console.log(`Generated ${files.length} contract type declarations.`); } diff --git a/demo/ai-todo-assistant/core/architecture/validate-feature-definitions.ts b/demo/ai-todo-assistant/core/architecture/validate-feature-definitions.ts index c75bb03..2a339c6 100644 --- a/demo/ai-todo-assistant/core/architecture/validate-feature-definitions.ts +++ b/demo/ai-todo-assistant/core/architecture/validate-feature-definitions.ts @@ -4,17 +4,39 @@ import { dirname, join, relative, resolve } from "node:path"; import { fileURLToPath, pathToFileURL } from "node:url"; import { createRequire } from "node:module"; -import { CapabilityName, FeatureName, HttpMethod, PermissionScope, SlotName } from "../shared/architecture-enums.js"; +import { + CapabilityName, + DomainEventType, + HttpMethod, + PermissionScope, + SlotName, +} from "../shared/architecture-enums.js"; +import { FeatureName } from "../shared/generated/feature-names.js"; interface FeatureDefinition { name: string; dependsOn: string[]; consumesContracts: string[]; events: string[]; - backend: { route: string; method: string; capabilities: string[]; mcp: { exposed: boolean; toolName?: string } }; - frontend: { slots: string[] }; + backend: { + route: string; + method: string; + capabilities: string[]; + mcp: { exposed: boolean; toolName?: string }; + }; + frontend: { + slots: string[]; + eventConsumers?: { name: string; eventType: string; contract: string }[]; + }; contracts: { input: string; output: string; mcpInput?: string }; - security: { mutation: boolean; actorRequired: boolean; idempotent: boolean; requiresConfirmation: boolean; mcpScopes: string[]; concurrency: "none" | "optimistic" }; + security: { + mutation: boolean; + actorRequired: boolean; + idempotent: boolean; + requiresConfirmation: boolean; + mcpScopes: string[]; + concurrency: "none" | "optimistic"; + }; } const appRoot = resolve(dirname(fileURLToPath(import.meta.url)), "../.."); @@ -23,13 +45,18 @@ const require = createRequire(import.meta.url); const Ajv2020 = require("ajv/dist/2020").default; const addFormats = require("ajv-formats").default; -async function findFiles(directory: string, predicate: (file: string) => boolean): Promise { +async function findFiles( + directory: string, + predicate: (file: string) => boolean, +): Promise { const entries = await readdir(directory, { withFileTypes: true }); - const files = await Promise.all(entries.map(async (entry) => { - const location = join(directory, entry.name); - if (entry.isDirectory()) return findFiles(location, predicate); - return predicate(location) ? [location] : []; - })); + const files = await Promise.all( + entries.map(async (entry) => { + const location = join(directory, entry.name); + if (entry.isDirectory()) return findFiles(location, predicate); + return predicate(location) ? [location] : []; + }), + ); return files.flat(); } @@ -38,7 +65,8 @@ async function readJson(file: string): Promise { } function assertEnumValue(value: string, values: object, label: string): void { - if (!Object.values(values).includes(value)) throw new Error(`${label} has unknown enum value '${value}'`); + if (!Object.values(values).includes(value)) + throw new Error(`${label} has unknown enum value '${value}'`); } function assertExistingFile(base: string, reference: string, label: string): string { @@ -52,7 +80,8 @@ function assertAcyclicFeatureDependencies(features: FeatureDefinition[]): void { const visiting = new Set(); const visited = new Set(); const visit = (name: string, path: string[]): void => { - if (visiting.has(name)) throw new Error(`Circular feature dependency: ${[...path, name].join(" -> ")}`); + if (visiting.has(name)) + throw new Error(`Circular feature dependency: ${[...path, name].join(" -> ")}`); if (visited.has(name)) return; const feature = byName.get(name); if (!feature) throw new Error(`Feature '${path.at(-1)}' depends on unknown feature '${name}'`); @@ -65,48 +94,105 @@ function assertAcyclicFeatureDependencies(features: FeatureDefinition[]): void { } async function main(): Promise { - const definitionSchema = await readJson(join(appRoot, "core/architecture/feature-definition.schema.json")); + const definitionSchema = await readJson( + join(appRoot, "core/architecture/feature-definition.schema.json"), + ); const schemaFiles = await findFiles(contextsRoot, (file) => file.endsWith(".schema.json")); const ajv = new Ajv2020({ allErrors: true, strict: true }); addFormats(ajv); - for (const schemaFile of schemaFiles) ajv.addSchema(await readJson(schemaFile), pathToFileURL(schemaFile).href); + for (const schemaFile of schemaFiles) + ajv.addSchema(await readJson(schemaFile), pathToFileURL(schemaFile).href); const validateDefinition = ajv.compile(definitionSchema); - for (const schemaFile of schemaFiles) ajv.getSchema(pathToFileURL(schemaFile).href) ?? ajv.compile(await readJson(schemaFile)); + for (const schemaFile of schemaFiles) + ajv.getSchema(pathToFileURL(schemaFile).href) ?? + ajv.compile(await readJson(schemaFile)); - const definitionFiles = await findFiles(contextsRoot, (file) => file.endsWith("feature.definition.json")); - const definitions = await Promise.all(definitionFiles.map(async (file) => ({ file, definition: await readJson(file) }))); + const definitionFiles = await findFiles(contextsRoot, (file) => + file.endsWith("feature.definition.json"), + ); + const definitions = await Promise.all( + definitionFiles.map(async (file) => ({ + file, + definition: await readJson(file), + })), + ); const routes = new Set(); const tools = new Set(); for (const { file, definition } of definitions) { - if (!validateDefinition(definition)) throw new Error(`${relative(appRoot, file)} fails schema validation: ${ajv.errorsText(validateDefinition.errors)}`); + if (!validateDefinition(definition)) + throw new Error( + `${relative(appRoot, file)} fails schema validation: ${ajv.errorsText(validateDefinition.errors)}`, + ); assertEnumValue(definition.name, FeatureName, definition.name); assertEnumValue(definition.backend.method, HttpMethod, definition.name); - for (const capability of definition.backend.capabilities) assertEnumValue(capability, CapabilityName, definition.name); + for (const capability of definition.backend.capabilities) + assertEnumValue(capability, CapabilityName, definition.name); for (const slot of definition.frontend.slots) assertEnumValue(slot, SlotName, definition.name); - for (const scope of definition.security.mcpScopes) assertEnumValue(scope, PermissionScope, definition.name); - if (!definition.security.actorRequired || !definition.security.idempotent) throw new Error(`${definition.name} must require an actor and be idempotent`); - if (definition.backend.mcp.exposed && definition.security.mcpScopes.length === 0) throw new Error(`${definition.name} exposes MCP without a scope`); - if (definition.backend.method === HttpMethod.Delete && !definition.security.requiresConfirmation) throw new Error(`${definition.name} deletion requires confirmation`); + for (const consumer of definition.frontend.eventConsumers ?? []) { + assertEnumValue(consumer.eventType, DomainEventType, definition.name); + assertExistingFile(contextsRoot, consumer.contract, definition.name); + if (!definition.consumesContracts.includes(consumer.contract)) + throw new Error( + `${definition.name} frontend event consumer must declare '${consumer.contract}' in consumesContracts`, + ); + } + for (const scope of definition.security.mcpScopes) + assertEnumValue(scope, PermissionScope, definition.name); + if (!definition.security.actorRequired || !definition.security.idempotent) + throw new Error(`${definition.name} must require an actor and be idempotent`); + if (definition.backend.mcp.exposed && definition.security.mcpScopes.length === 0) + throw new Error(`${definition.name} exposes MCP without a scope`); + if ( + definition.backend.method === HttpMethod.Delete && + !definition.security.requiresConfirmation + ) + throw new Error(`${definition.name} deletion requires confirmation`); const routeKey = `${definition.backend.method} ${definition.backend.route}`; if (routes.has(routeKey)) throw new Error(`Duplicate route ${routeKey}`); routes.add(routeKey); if (definition.backend.mcp.exposed) { - if (!definition.backend.mcp.toolName) throw new Error(`${definition.name} exposes MCP without a tool name`); - if (tools.has(definition.backend.mcp.toolName)) throw new Error(`Duplicate MCP tool ${definition.backend.mcp.toolName}`); + if (!definition.backend.mcp.toolName) + throw new Error(`${definition.name} exposes MCP without a tool name`); + if (tools.has(definition.backend.mcp.toolName)) + throw new Error(`Duplicate MCP tool ${definition.backend.mcp.toolName}`); tools.add(definition.backend.mcp.toolName); } const featureDirectory = dirname(file); - const inputSchema = await readJson<{ required?: string[] }>(assertExistingFile(featureDirectory, definition.contracts.input, definition.name)); + const inputSchema = await readJson<{ required?: string[] }>( + assertExistingFile(featureDirectory, definition.contracts.input, definition.name), + ); assertExistingFile(featureDirectory, definition.contracts.output, definition.name); - if (definition.contracts.mcpInput) assertExistingFile(featureDirectory, definition.contracts.mcpInput, definition.name); - if (definition.security.mutation && !inputSchema.required?.includes("idempotencyKey")) throw new Error(`${definition.name} mutation input requires idempotencyKey`); - if (definition.security.concurrency === "optimistic" && !inputSchema.required?.includes("expectedVersion")) throw new Error(`${definition.name} optimistic mutation input requires expectedVersion`); - for (const contract of [...definition.consumesContracts, ...definition.events]) assertExistingFile(contextsRoot, contract, definition.name); + if (definition.contracts.mcpInput) + assertExistingFile(featureDirectory, definition.contracts.mcpInput, definition.name); + const contributionFile = join(featureDirectory, "frontend", "contribution.tsx"); + const contributionSource = await readFile(contributionFile, "utf8"); + const implementedSlots = new Set( + [...contributionSource.matchAll(/slot:\s*SlotName\.([A-Za-z0-9_]+)/g)].map( + (match) => match[1], + ), + ); + const declaredSlots = new Set(definition.frontend.slots); + if ( + [...implementedSlots].some((slot) => !declaredSlots.has(slot)) || + [...declaredSlots].some((slot) => !implementedSlots.has(slot)) + ) + throw new Error(`${definition.name} frontend slots do not match executable contributions`); + if (inputSchema.required?.includes("idempotencyKey")) + throw new Error(`${definition.name} must keep idempotency in command metadata`); + if ( + definition.security.concurrency === "optimistic" && + !inputSchema.required?.includes("expectedVersion") + ) + throw new Error(`${definition.name} optimistic mutation input requires expectedVersion`); + for (const contract of [...definition.consumesContracts, ...definition.events]) + assertExistingFile(contextsRoot, contract, definition.name); } assertAcyclicFeatureDependencies(definitions.map(({ definition }) => definition)); - console.log(`Validated ${definitions.length} feature definitions, schemas, contracts, routes, MCP tools, enums, and dependency graph.`); + console.log( + `Validated ${definitions.length} feature definitions, schemas, contracts, routes, MCP tools, enums, and dependency graph.`, + ); } void main(); diff --git a/demo/ai-todo-assistant/core/backend/auth/actor-resolver.ts b/demo/ai-todo-assistant/core/backend/auth/actor-resolver.ts new file mode 100644 index 0000000..e63f5fa --- /dev/null +++ b/demo/ai-todo-assistant/core/backend/auth/actor-resolver.ts @@ -0,0 +1,50 @@ +import type { IncomingHttpHeaders } from "node:http"; +import { createRemoteJWKSet, jwtVerify } from "jose"; +import { PermissionScope } from "../../shared/architecture-enums.js"; +import type { Actor } from "../../shared/aifa.js"; +import { config, RuntimeEnvironment } from "../config.js"; + +const validScopes = new Set(Object.values(PermissionScope)); +const oidcJwks = config.oidcJwksUrl ? createRemoteJWKSet(new URL(config.oidcJwksUrl)) : undefined; + +function actor( + tenantId: string | undefined, + userId: string | undefined, + scopes: readonly string[], +): Actor | null { + return tenantId && userId + ? { + tenantId, + userId, + scopes: scopes.filter((scope): scope is PermissionScope => + validScopes.has(scope as PermissionScope), + ), + } + : null; +} + +export async function resolveActor(headers: IncomingHttpHeaders): Promise { + if (config.nodeEnv !== RuntimeEnvironment.Production) { + const tenantId = headers["x-aifa-tenant-id"]?.toString() ?? config.demoTenantId; + const userId = headers["x-aifa-user-id"]?.toString() ?? config.demoUserId; + const scopes = + headers["x-aifa-scopes"]?.toString().split(",") ?? Object.values(PermissionScope); + return actor(tenantId, userId, scopes); + } + const token = headers.authorization?.match(/^Bearer (.+)$/i)?.[1]; + if (!token || !oidcJwks || !config.oidcIssuerUrl || !config.oidcAudience) return null; + try { + const { payload } = await jwtVerify(token, oidcJwks, { + issuer: config.oidcIssuerUrl, + audience: config.oidcAudience, + }); + const scopes = typeof payload.scope === "string" ? payload.scope.split(" ") : []; + return actor( + typeof payload.tenant_id === "string" ? payload.tenant_id : undefined, + payload.sub, + scopes, + ); + } catch { + return null; + } +} diff --git a/demo/ai-todo-assistant/core/backend/config.ts b/demo/ai-todo-assistant/core/backend/config.ts new file mode 100644 index 0000000..a12b33b --- /dev/null +++ b/demo/ai-todo-assistant/core/backend/config.ts @@ -0,0 +1,57 @@ +import "dotenv/config"; + +export enum RuntimeEnvironment { + Development = "development", + Test = "test", + Production = "production", +} + +const nodeEnv = (process.env.NODE_ENV ?? RuntimeEnvironment.Development) as RuntimeEnvironment; +if (!Object.values(RuntimeEnvironment).includes(nodeEnv)) + throw new Error("NODE_ENV must be development, test, or production"); + +export const config = { + nodeEnv, + host: process.env.HOST ?? "127.0.0.1", + port: Number(process.env.PORT ?? 3000), + mongoDatabase: process.env.MONGODB_DATABASE ?? "ai_todo_assistant", + mongoUri: process.env.MONGODB_URI, + ollamaBaseUrl: process.env.OLLAMA_BASE_URL ?? "http://127.0.0.1:11434", + ollamaModel: process.env.OLLAMA_MODEL ?? "llama3.2", + oidcIssuerUrl: process.env.OIDC_ISSUER_URL, + oidcAudience: process.env.OIDC_AUDIENCE, + oidcJwksUrl: process.env.OIDC_JWKS_URL, + mcpAccessToken: process.env.MCP_ACCESS_TOKEN, + demoTenantId: process.env.DEMO_TENANT_ID, + demoUserId: process.env.DEMO_USER_ID, + idempotencyLeaseMs: Number(process.env.IDEMPOTENCY_LEASE_MS ?? 300_000), + corsOrigin: process.env.CORS_ORIGIN ?? "http://127.0.0.1:5173", + publicBaseUrl: process.env.PUBLIC_BASE_URL ?? `http://${process.env.HOST ?? "127.0.0.1"}:${Number(process.env.PORT ?? 3000)}`, + requestTimeoutMs: Number(process.env.REQUEST_TIMEOUT_MS ?? 30_000), +} as const; + +if (!Number.isSafeInteger(config.port) || config.port < 1 || config.port > 65_535) + throw new Error("PORT must be an integer between 1 and 65535"); +if (!Number.isSafeInteger(config.idempotencyLeaseMs) || config.idempotencyLeaseMs < 1_000) + throw new Error("IDEMPOTENCY_LEASE_MS must be a positive integer of at least 1000"); +if (!Number.isSafeInteger(config.requestTimeoutMs) || config.requestTimeoutMs < 1_000) + throw new Error("REQUEST_TIMEOUT_MS must be an integer of at least 1000"); +for (const [name, value] of [ + ["OLLAMA_BASE_URL", config.ollamaBaseUrl], + ["CORS_ORIGIN", config.corsOrigin], + ["PUBLIC_BASE_URL", config.publicBaseUrl], +] as const) { + try { + new URL(value); + } catch { + throw new Error(`${name} must be an absolute URL`); + } +} + +if ( + config.nodeEnv === RuntimeEnvironment.Production && + (!config.mongoUri || !config.oidcIssuerUrl || !config.oidcAudience || !config.oidcJwksUrl) +) + throw new Error( + "Production requires MONGODB_URI, OIDC_ISSUER_URL, OIDC_AUDIENCE, and OIDC_JWKS_URL", + ); diff --git a/demo/ai-todo-assistant/core/backend/database/mongo.ts b/demo/ai-todo-assistant/core/backend/database/mongo.ts new file mode 100644 index 0000000..bc5f923 --- /dev/null +++ b/demo/ai-todo-assistant/core/backend/database/mongo.ts @@ -0,0 +1,41 @@ +import { MongoClient, type ClientSession, type Db } from "mongodb"; +import type { TransactionRunner } from "./transaction-runner.js"; + +export interface MongoDatabase extends TransactionRunner { + connect(): Promise; + close(): Promise; +} + +export function createMongoDatabase(uri: string, databaseName: string): MongoDatabase { + const client = new MongoClient(uri); + let database: Db | undefined; + + async function connect(): Promise { + if (!database) { + await client.connect(); + database = client.db(databaseName); + await database.command({ ping: 1 }); + } + return database; + } + + async function close(): Promise { + await client.close(); + database = undefined; + } + async function withTransaction( + work: (session: ClientSession) => Promise, + ): Promise { + const session = client.startSession(); + try { + let value: Value | undefined; + await session.withTransaction(async () => { + value = await work(session); + }); + return value as Value; + } finally { + await session.endSession(); + } + } + return { connect, close, withTransaction }; +} diff --git a/demo/ai-todo-assistant/core/backend/database/transaction-runner.ts b/demo/ai-todo-assistant/core/backend/database/transaction-runner.ts new file mode 100644 index 0000000..f0b9bba --- /dev/null +++ b/demo/ai-todo-assistant/core/backend/database/transaction-runner.ts @@ -0,0 +1,3 @@ +export interface TransactionRunner { + withTransaction(work: (context: Context) => Promise): Promise; +} diff --git a/demo/ai-todo-assistant/core/backend/discovery/context-infrastructure.ts b/demo/ai-todo-assistant/core/backend/discovery/context-infrastructure.ts new file mode 100644 index 0000000..29375fc --- /dev/null +++ b/demo/ai-todo-assistant/core/backend/discovery/context-infrastructure.ts @@ -0,0 +1,43 @@ +import { access, readdir } from "node:fs/promises"; +import { join } from "node:path"; +import { pathToFileURL } from "node:url"; +import type { ClientSession, Db } from "mongodb"; +import type { Actor, RequestMetadata } from "../../shared/aifa.js"; +import type { CapabilityName } from "../../shared/architecture-enums.js"; + +export interface InfrastructureContext { + database: Db; + actor: Actor; + metadata: RequestMetadata; + session?: ClientSession; + settings: Readonly>; +} + +export interface ContextInfrastructureManifest { + name: string; + externalCapabilities?: readonly CapabilityName[]; + ensureIndexes(database: Db): Promise; + createCapabilities(context: InfrastructureContext): Record; + health?(): Record; +} + +export async function discoverContextInfrastructure( + contextsRoot: string, +): Promise { + const contexts = await readdir(contextsRoot, { withFileTypes: true }); + const manifests: ContextInfrastructureManifest[] = []; + for (const context of contexts.filter((entry) => entry.isDirectory())) { + const sourceFile = join(contextsRoot, context.name, "infrastructure", "manifest.ts"); + const builtFile = join(contextsRoot, context.name, "infrastructure", "manifest.js"); + const file = await access(builtFile).then(() => builtFile).catch(() => sourceFile); + try { + const loaded = (await import(pathToFileURL(file).href)) as { + infrastructureManifest?: ContextInfrastructureManifest; + }; + if (loaded.infrastructureManifest) manifests.push(loaded.infrastructureManifest); + } catch (cause) { + if ((cause as NodeJS.ErrnoException).code !== "ERR_MODULE_NOT_FOUND") throw cause; + } + } + return manifests.sort((left, right) => left.name.localeCompare(right.name)); +} diff --git a/demo/ai-todo-assistant/core/backend/discovery/discover-features.ts b/demo/ai-todo-assistant/core/backend/discovery/discover-features.ts new file mode 100644 index 0000000..b06221b --- /dev/null +++ b/demo/ai-todo-assistant/core/backend/discovery/discover-features.ts @@ -0,0 +1,181 @@ +import { access, readdir, readFile } from "node:fs/promises"; +import { dirname, join } from "node:path"; +import { pathToFileURL } from "node:url"; +import { createRequire } from "node:module"; +import type { ErrorObject, ValidateFunction } from "ajv"; +import type { + ContractValidationError, + FeatureManifest, +} from "../../shared/feature-manifest.js"; +const require = createRequire(import.meta.url); +const Ajv2020 = require("ajv/dist/2020").default as typeof import("ajv/dist/2020.js").Ajv2020; +const addFormats = require("ajv-formats").default as (ajv: InstanceType) => void; + +interface FeatureDefinition { + name: string; + businessNeed: string; + backend: { + route: string; + method: string; + capabilities: string[]; + mcp: { exposed: boolean; toolName?: string }; + }; + frontend: { + slots: string[]; + eventConsumers?: { name: string; eventType: string; contract: string }[]; + }; + contracts: { input: string; output: string; mcpInput?: string }; + events: string[]; + security: { mcpScopes: string[]; requiresConfirmation: boolean }; +} + +function isFeatureManifest(value: unknown): value is FeatureManifest { + if (!value || typeof value !== "object") return false; + const manifest = value as Partial; + return ( + typeof manifest.name === "string" && + typeof manifest.backend?.route === "string" && + typeof manifest.backend?.method === "string" && + Array.isArray(manifest.backend?.requiredScopes) && + typeof manifest.backend?.feature?.execute === "function" && + Array.isArray(manifest.frontend?.contributions) + ); +} + +async function findFiles(directory: string, name: string): Promise { + const entries = await readdir(directory, { withFileTypes: true }); + const nested = await Promise.all( + entries.map(async (entry) => { + const file = join(directory, entry.name); + if (entry.isDirectory()) return findFiles(file, name); + return entry.name === name || (name === "*.schema.json" && entry.name.endsWith(".schema.json")) + ? [file] + : []; + }), + ); + return nested.flat(); +} + +function validationErrors(errors: ErrorObject[] | null | undefined): ContractValidationError[] { + return (errors ?? []).map((error) => ({ + path: error.instancePath || "/", + message: error.message ?? "is invalid", + })); +} + +function validator(validate: ValidateFunction) { + return (value: unknown): ContractValidationError[] => + validate(value) ? [] : validationErrors(validate.errors); +} + +function assertParity(definition: FeatureDefinition, manifest: FeatureManifest, file: string): void { + const failures: string[] = []; + if (definition.name !== manifest.name) failures.push("name"); + if (definition.backend.route !== manifest.backend.route) failures.push("route"); + if (definition.backend.method !== manifest.backend.method) failures.push("method"); + if ( + [...definition.backend.capabilities].sort().join() !== + [...manifest.backend.feature.capabilities].sort().join() + ) + failures.push("capabilities"); + if ( + [...definition.security.mcpScopes].sort().join() !== + [...manifest.backend.requiredScopes].sort().join() + ) + failures.push("scopes"); + const slots = new Set(manifest.frontend.contributions.map(({ slot }) => slot)); + if ( + definition.frontend.slots.some((slot) => ![...slots].includes(slot as never)) || + [...slots].some((slot) => !definition.frontend.slots.includes(slot)) + ) + failures.push("frontend slots"); + const declaredConsumers = (definition.frontend.eventConsumers ?? []) + .map(({ name, eventType, contract }) => `${name}:${eventType}:${contract}`) + .sort(); + const executableConsumers = (manifest.frontend.eventConsumers ?? []) + .map(({ name, eventType, contract }) => `${name}:${eventType}:${contract}`) + .sort(); + if (declaredConsumers.join() !== executableConsumers.join()) + failures.push("frontend event consumers"); + if (definition.backend.mcp.exposed !== Boolean(manifest.mcp)) failures.push("MCP exposure"); + if (definition.backend.mcp.toolName !== manifest.mcp?.toolName) failures.push("MCP tool name"); + if (failures.length) + throw new Error(`Feature definition/manifest mismatch (${failures.join(", ")}): ${file}`); +} + +export async function discoverFeatureManifests(root: string): Promise { + const ajv = new Ajv2020({ allErrors: true, strict: false }); + addFormats(ajv); + for (const file of await findFiles(root, "*.schema.json")) { + const schema = JSON.parse(await readFile(file, "utf8")) as Record; + if (typeof schema.$id === "string" && !ajv.getSchema(schema.$id)) ajv.addSchema(schema); + } + + const manifests: FeatureManifest[] = []; + for (const definitionFile of (await findFiles(root, "feature.definition.json")).sort()) { + const definition = JSON.parse(await readFile(definitionFile, "utf8")) as FeatureDefinition; + const featureDirectory = dirname(definitionFile); + const sourceManifest = join(featureDirectory, "manifest.ts"); + const builtManifest = join(featureDirectory, "manifest.js"); + const manifestFile = await access(builtManifest).then(() => builtManifest).catch(() => sourceManifest); + const loaded = (await import(pathToFileURL(manifestFile).href)) as { manifest?: unknown }; + if (!isFeatureManifest(loaded.manifest)) throw new Error(`Invalid feature manifest: ${manifestFile}`); + assertParity(definition, loaded.manifest, definitionFile); + + const inputSchema = JSON.parse( + await readFile(join(featureDirectory, definition.contracts.input), "utf8"), + ) as Record; + const outputSchema = JSON.parse( + await readFile(join(featureDirectory, definition.contracts.output), "utf8"), + ) as Record; + const eventValidators = new Map(); + for (const eventFile of definition.events) { + const eventSchema = JSON.parse(await readFile(join(root, eventFile), "utf8")) as Record< + string, + unknown + >; + const eventType = (eventSchema.properties as Record | undefined) + ?.eventType?.const; + if (eventType) eventValidators.set(eventType, ajv.compile(eventSchema)); + } + manifests.push({ + ...loaded.manifest, + backend: { + ...loaded.manifest.backend, + contract: { + inputSchema, + outputSchema, + validateInput: validator(ajv.compile(inputSchema)), + validateOutput: validator(ajv.compile(outputSchema)), + validateEvent(value) { + const eventType = + value && typeof value === "object" + ? (value as Record).eventType + : undefined; + const validate = typeof eventType === "string" ? eventValidators.get(eventType) : undefined; + return validate + ? validator(validate)(value) + : [{ path: "/eventType", message: "is not declared by this feature" }]; + }, + }, + }, + ...(loaded.manifest.mcp + ? { + mcp: { + ...loaded.manifest.mcp, + description: definition.businessNeed, + requiresConfirmation: definition.security.requiresConfirmation, + }, + } + : {}), + }); + } + + const duplicate = (values: string[]) => values.find((value, index) => values.indexOf(value) !== index); + const duplicateName = duplicate(manifests.map(({ name }) => name)); + const duplicateRoute = duplicate(manifests.map(({ backend }) => `${backend.method} ${backend.route}`)); + const duplicateTool = duplicate(manifests.flatMap(({ mcp }) => (mcp ? [mcp.toolName] : []))); + if (duplicateName || duplicateRoute || duplicateTool) + throw new Error(`Duplicate feature registration: ${duplicateName ?? duplicateRoute ?? duplicateTool}`); + return manifests.sort((left, right) => left.name.localeCompare(right.name)); +} diff --git a/demo/ai-todo-assistant/core/backend/http/authorization-middleware.ts b/demo/ai-todo-assistant/core/backend/http/authorization-middleware.ts new file mode 100644 index 0000000..18f279e --- /dev/null +++ b/demo/ai-todo-assistant/core/backend/http/authorization-middleware.ts @@ -0,0 +1,38 @@ +import type { IncomingHttpHeaders } from "node:http"; +import type { Actor } from "../../shared/aifa.js"; +import { ErrorCode, type PermissionScope } from "../../shared/architecture-enums.js"; + +export type ActorResolver = (headers: IncomingHttpHeaders) => Actor | null | Promise; + +export type AuthorizationResult = + | { ok: true; actor: Actor } + | { + ok: false; + status: 401 | 403; + code: ErrorCode.NotAuthorized | ErrorCode.Forbidden; + message: string; + }; + +export function createAuthorizationMiddleware(resolveActor: ActorResolver) { + return async ( + headers: IncomingHttpHeaders, + requiredScopes: readonly PermissionScope[], + ): Promise => { + const actor = await resolveActor(headers); + if (!actor) + return { + ok: false, + status: 401, + code: ErrorCode.NotAuthorized, + message: "An actor is required", + }; + if (!requiredScopes.every((scope) => actor.scopes.includes(scope))) + return { + ok: false, + status: 403, + code: ErrorCode.Forbidden, + message: "The actor lacks a required permission scope", + }; + return { ok: true, actor }; + }; +} diff --git a/demo/ai-todo-assistant/core/backend/http/request-handler.ts b/demo/ai-todo-assistant/core/backend/http/request-handler.ts new file mode 100644 index 0000000..cd1dd90 --- /dev/null +++ b/demo/ai-todo-assistant/core/backend/http/request-handler.ts @@ -0,0 +1,170 @@ +import type { IncomingMessage, ServerResponse } from "node:http"; +import type { Actor, FeatureResult } from "../../shared/aifa.js"; +import { ErrorCode, HttpMethod } from "../../shared/architecture-enums.js"; +import type { FeatureManifest } from "../../shared/feature-manifest.js"; +import { createAuthorizationMiddleware, type ActorResolver } from "./authorization-middleware.js"; +import { parseRequestInput } from "./request-parser.js"; +import { matchRoute } from "./route-matcher.js"; +import { logger } from "../telemetry/logger.js"; + +export interface HttpDependencies { + manifests: readonly FeatureManifest[]; + corsOrigin?: string; + resolveActor: ActorResolver; + readiness?: () => Promise>; + requestTimeoutMs?: number; + run( + manifest: FeatureManifest, + input: Record, + actor: Actor, + metadata: { correlationId: string; causationId: string; idempotencyKey?: string }, + ): Promise>; +} + +function statusFor(result: FeatureResult): number { + if (result.ok) return 200; + if (result.error.code === ErrorCode.NotFound) return 404; + if (result.error.code === ErrorCode.ConfirmationRequired) return 428; + if ( + result.error.code === ErrorCode.InvalidInput || + result.error.code === ErrorCode.InvalidStatusTransition || + result.error.code === ErrorCode.AssistantResponseInvalid + ) + return 422; + if ( + result.error.code === ErrorCode.VersionConflict || + result.error.code === ErrorCode.IdempotencyKeyReused || + result.error.code === ErrorCode.RequestInProgress + ) + return 409; + if (result.error.code === ErrorCode.InternalError || result.error.code === ErrorCode.InvalidOutput) + return 500; + if (result.error.code === ErrorCode.ProviderUnavailable) return 503; + return 400; +} + +function writeJson(response: ServerResponse, status: number, value: unknown): void { + response.writeHead(status, { "content-type": "application/json" }).end(JSON.stringify(value)); +} + +export function createRequestHandler(dependencies: HttpDependencies) { + const authorize = createAuthorizationMiddleware(dependencies.resolveActor); + return async (request: IncomingMessage, response: ServerResponse): Promise => { + const correlationId = request.headers["x-correlation-id"]?.toString() ?? crypto.randomUUID(); + const startedAt = performance.now(); + response.once("finish", () => + logger.info("request_complete", { + correlationId, + method: request.method, + path: request.url?.split("?")[0], + status: response.statusCode, + durationMs: Math.round(performance.now() - startedAt), + }), + ); + request.setTimeout(dependencies.requestTimeoutMs ?? 30_000, () => + request.destroy(new Error("Request deadline exceeded")), + ); + response.setHeader("x-correlation-id", correlationId); + try { + if (dependencies.corsOrigin) { + response.setHeader("access-control-allow-origin", dependencies.corsOrigin); + response.setHeader( + "access-control-allow-headers", + "content-type, idempotency-key, x-aifa-tenant-id, x-aifa-user-id, x-aifa-scopes, authorization", + ); + response.setHeader("access-control-allow-methods", "GET, POST, PATCH, DELETE, OPTIONS"); + } + if (request.method === "OPTIONS") return void response.writeHead(204).end(); + if (request.url === "/health" || request.url === "/health/live") + return writeJson(response, 200, { status: "ok" }); + if (request.url === "/health/ready") { + try { + const checks = await dependencies.readiness?.(); + return writeJson(response, 200, { status: "ready", checks }); + } catch { + return writeJson(response, 503, { status: "not-ready" }); + } + } + const url = new URL(request.url ?? "/", `http://${request.headers.host ?? "127.0.0.1"}`); + const matched = matchRoute(dependencies.manifests, request.method, url.pathname); + if (!matched) return writeJson(response, 404, { ok: false }); + let body: Record; + try { + body = await parseRequestInput(request, url); + } catch { + return writeJson(response, 400, { + ok: false, + error: { code: ErrorCode.InvalidJson, message: "Request must be a bounded JSON object" }, + }); + } + const authorization = await authorize(request.headers, matched.manifest.backend.requiredScopes); + if (!authorization.ok) + return writeJson(response, authorization.status, { + ok: false, + error: { code: authorization.code, message: authorization.message }, + }); + const idempotencyKey = + request.headers["idempotency-key"]?.toString() ?? body.idempotencyKey?.toString(); + delete body.idempotencyKey; + if ( + matched.manifest.backend.method !== HttpMethod.Get && + (!idempotencyKey || idempotencyKey.length < 16) + ) + return writeJson(response, 400, { + ok: false, + error: { + code: ErrorCode.InvalidInput, + message: "Mutations require an idempotency key", + }, + }); + const input = { ...body, ...matched.params }; + const inputErrors = matched.manifest.backend.contract?.validateInput(input) ?? []; + if (inputErrors.length) + return writeJson(response, 400, { + ok: false, + error: { + code: ErrorCode.InvalidInput, + message: "Input does not satisfy the feature contract", + details: { fields: inputErrors }, + }, + }); + const result = await dependencies.run( + matched.manifest, + input, + authorization.actor, + { + correlationId, + causationId: request.headers["x-causation-id"]?.toString() ?? correlationId, + idempotencyKey, + }, + ); + if (!result.ok) return writeJson(response, statusFor(result), result); + const value = matched.manifest.backend.mapOutput + ? matched.manifest.backend.mapOutput(result.value) + : result.value; + const outputErrors = matched.manifest.backend.contract?.validateOutput(value) ?? []; + if (outputErrors.length) + return writeJson(response, 500, { + ok: false, + error: { + code: ErrorCode.InvalidOutput, + message: "Feature output violated its contract", + details: { correlationId }, + }, + }); + return writeJson(response, 200, { ok: true, value }); + } catch (cause) { + logger.error("request_failed", { + correlationId, + error: cause instanceof Error ? cause.message : "Unknown error", + durationMs: Math.round(performance.now() - startedAt), + }); + if (!response.headersSent) + writeJson(response, 500, { + ok: false, + error: { code: ErrorCode.InternalError, message: "Request failed", details: { correlationId } }, + }); + else response.end(); + } + }; +} diff --git a/demo/ai-todo-assistant/core/backend/http/request-parser.ts b/demo/ai-todo-assistant/core/backend/http/request-parser.ts new file mode 100644 index 0000000..cebe296 --- /dev/null +++ b/demo/ai-todo-assistant/core/backend/http/request-parser.ts @@ -0,0 +1,23 @@ +import type { IncomingMessage } from "node:http"; + +export async function parseRequestInput( + request: IncomingMessage, + url: URL, + maximumBytes = 1_048_576, +): Promise> { + const chunks: Buffer[] = []; + let size = 0; + for await (const chunk of request) { + const buffer = Buffer.from(chunk); + size += buffer.byteLength; + if (size > maximumBytes) throw new Error("Request body is too large"); + chunks.push(buffer); + } + if (!chunks.length) return Object.fromEntries(url.searchParams); + if (!request.headers["content-type"]?.toString().toLowerCase().startsWith("application/json")) + throw new Error("Request content type must be application/json"); + const parsed: unknown = JSON.parse(Buffer.concat(chunks).toString("utf8")); + if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) + throw new Error("Request body must be a JSON object"); + return parsed as Record; +} diff --git a/demo/ai-todo-assistant/core/backend/http/route-matcher.ts b/demo/ai-todo-assistant/core/backend/http/route-matcher.ts new file mode 100644 index 0000000..5350d2b --- /dev/null +++ b/demo/ai-todo-assistant/core/backend/http/route-matcher.ts @@ -0,0 +1,28 @@ +import type { FeatureManifest } from "../../shared/feature-manifest.js"; + +export interface MatchedRoute { + manifest: FeatureManifest; + params: Record; +} + +export function matchRoute( + manifests: readonly FeatureManifest[], + method: string | undefined, + pathname: string, +): MatchedRoute | null { + for (const manifest of manifests) { + if (manifest.backend.method !== method) continue; + const names = [...manifest.backend.route.matchAll(/:([^/]+)/g)].map((match) => match[1]); + const match = pathname.match( + new RegExp(`^${manifest.backend.route.replace(/:[^/]+/g, "([^/]+)")}$`), + ); + if (!match) continue; + return { + manifest, + params: Object.fromEntries( + names.map((name, index) => [name, decodeURIComponent(match[index + 1]!)]), + ), + }; + } + return null; +} diff --git a/demo/ai-todo-assistant/core/backend/idempotency/canonical-fingerprint.ts b/demo/ai-todo-assistant/core/backend/idempotency/canonical-fingerprint.ts new file mode 100644 index 0000000..3dbf962 --- /dev/null +++ b/demo/ai-todo-assistant/core/backend/idempotency/canonical-fingerprint.ts @@ -0,0 +1,18 @@ +import { createHash } from "node:crypto"; + +function canonicalize(value: unknown): unknown { + if (Array.isArray(value)) return value.map(canonicalize); + if (value && typeof value === "object") + return Object.fromEntries( + Object.entries(value as Record) + .sort(([left], [right]) => left.localeCompare(right)) + .map(([key, item]) => [key, canonicalize(item)]), + ); + return value; +} + +export function createCanonicalFingerprint(value: unknown): string { + return createHash("sha256") + .update(JSON.stringify(canonicalize(value))) + .digest("hex"); +} diff --git a/demo/ai-todo-assistant/core/backend/idempotency/mongo-idempotency.ts b/demo/ai-todo-assistant/core/backend/idempotency/mongo-idempotency.ts new file mode 100644 index 0000000..ef113fa --- /dev/null +++ b/demo/ai-todo-assistant/core/backend/idempotency/mongo-idempotency.ts @@ -0,0 +1,139 @@ +import type { ClientSession, Db } from "mongodb"; +import { randomUUID } from "node:crypto"; +import type { FeatureResult } from "../../shared/aifa.js"; +import type { FeatureName } from "../../shared/generated/feature-names.js"; + +interface IdempotencyRecord { + tenantId: string; + actorId: string; + featureName: FeatureName; + idempotencyKey: string; + inputFingerprint: string; + commandInput?: Record; + preparedCapabilities?: Record; + status: IdempotencyStatus; + result?: FeatureResult; + executionId: string; + claimedAt: string; + leaseExpiresAt: string; + createdAt: string; +} +export enum IdempotencyStatus { + Pending = "Pending", + Completed = "Completed", +} +export type IdempotencyClaim = + | { kind: "claimed"; executionId: string; preparedCapabilities: Record } + | { kind: "completed"; result: FeatureResult } + | { kind: "in-progress" } + | { kind: "key-reused" }; +export function createMongoIdempotencyStore(database: Db, leaseMs = 300_000) { + const records = database.collection("idempotency_records"); + return { + async claim( + record: Omit< + IdempotencyRecord, + "status" | "claimedAt" | "leaseExpiresAt" | "createdAt" | "result" | "executionId" + >, + ): Promise { + const filter = { + tenantId: record.tenantId, + actorId: record.actorId, + featureName: record.featureName, + idempotencyKey: record.idempotencyKey, + }; + try { + const executionId = randomUUID(); + const claimedAt = new Date(); + await records.insertOne({ + ...record, + status: IdempotencyStatus.Pending, + executionId, + claimedAt: claimedAt.toISOString(), + leaseExpiresAt: new Date(claimedAt.getTime() + leaseMs).toISOString(), + createdAt: claimedAt.toISOString(), + }); + return { kind: "claimed", executionId, preparedCapabilities: {} }; + } catch (error: unknown) { + if (!(error instanceof Error) || !("code" in error) || error.code !== 11000) throw error; + const existing = await records.findOne(filter); + if (!existing || existing.inputFingerprint !== record.inputFingerprint) + return { kind: "key-reused" }; + if (existing.status === IdempotencyStatus.Completed && existing.result) + return { kind: "completed", result: existing.result }; + const executionId = randomUUID(); + const claimedAt = new Date(); + const recovered = await records.updateOne( + { + ...filter, + status: IdempotencyStatus.Pending, + leaseExpiresAt: { $lte: claimedAt.toISOString() }, + }, + { + $set: { + executionId, + claimedAt: claimedAt.toISOString(), + leaseExpiresAt: new Date(claimedAt.getTime() + leaseMs).toISOString(), + }, + }, + ); + return recovered.modifiedCount === 1 + ? { + kind: "claimed", + executionId, + preparedCapabilities: existing.preparedCapabilities ?? {}, + } + : { kind: "in-progress" }; + } + }, + async heartbeat( + record: Pick, + executionId: string, + ): Promise { + const heartbeat = await records.updateOne( + { ...record, status: IdempotencyStatus.Pending, executionId }, + { $set: { leaseExpiresAt: new Date(Date.now() + leaseMs).toISOString() } }, + ); + if (heartbeat.modifiedCount !== 1) + throw new Error("Idempotency heartbeat lost its execution fence"); + }, + async savePreparedCapability( + record: Pick, + executionId: string, + capabilityName: string, + value: unknown, + ): Promise { + const saved = await records.updateOne( + { ...record, status: IdempotencyStatus.Pending, executionId }, + { + $set: { + [`preparedCapabilities.${capabilityName}`]: value, + leaseExpiresAt: new Date(Date.now() + leaseMs).toISOString(), + }, + }, + ); + if (saved.modifiedCount !== 1) + throw new Error("Prepared command result lost its execution fence"); + }, + async complete( + record: Pick, + executionId: string, + result: FeatureResult, + session?: ClientSession, + ) { + const completion = await records.updateOne( + { ...record, status: IdempotencyStatus.Pending, executionId }, + { $set: { status: IdempotencyStatus.Completed, result } }, + { session }, + ); + if (completion.modifiedCount !== 1) + throw new Error("Idempotency completion lost its execution fence"); + }, + async ensureIndexes() { + await records.createIndex( + { tenantId: 1, actorId: 1, featureName: 1, idempotencyKey: 1 }, + { unique: true, name: "idempotency_command" }, + ); + }, + }; +} diff --git a/demo/ai-todo-assistant/core/backend/mcp-http.ts b/demo/ai-todo-assistant/core/backend/mcp-http.ts new file mode 100644 index 0000000..fbcbd6a --- /dev/null +++ b/demo/ai-todo-assistant/core/backend/mcp-http.ts @@ -0,0 +1,72 @@ +import type { IncomingMessage, ServerResponse } from "node:http"; +import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js"; +import type { Actor } from "../shared/aifa.js"; +import type { FeatureManifest } from "../shared/feature-manifest.js"; +import type { ActorResolver } from "./http/authorization-middleware.js"; +import { createFeatureMcpServer } from "./mcp-server.js"; + +export function createRemoteMcpHandler(options: { + manifests: readonly FeatureManifest[]; + resolveActor: ActorResolver; + apiBaseUrl: string; + resourceUrl: string; + authorizationServer?: string; +}) { + const metadataUrl = new URL("/.well-known/oauth-protected-resource", options.resourceUrl).href; + function allowedFor(actor: Actor): FeatureManifest[] { + const scopes = new Set(actor.scopes); + return options.manifests.filter( + ({ mcp, backend }) => mcp && backend.requiredScopes.every((scope) => scopes.has(scope)), + ); + } + return async (request: IncomingMessage, response: ServerResponse): Promise => { + const pathname = new URL(request.url ?? "/", options.resourceUrl).pathname; + if (pathname === "/.well-known/oauth-protected-resource") { + response.writeHead(200, { "content-type": "application/json" }).end( + JSON.stringify({ + resource: options.resourceUrl, + ...(options.authorizationServer + ? { authorization_servers: [options.authorizationServer] } + : {}), + bearer_methods_supported: ["header"], + scopes_supported: [ + ...new Set(options.manifests.flatMap(({ backend }) => backend.requiredScopes)), + ], + }), + ); + return true; + } + if (pathname !== "/mcp") return false; + const actor = await options.resolveActor(request.headers); + if (!actor) { + response + .writeHead(401, { + "content-type": "application/json", + "www-authenticate": `Bearer resource_metadata="${metadataUrl}"`, + }) + .end(JSON.stringify({ error: "invalid_token" })); + return true; + } + if (request.method !== "POST") { + response.writeHead(405, { allow: "POST" }).end(); + return true; + } + const actorHeaders: Record = request.headers.authorization + ? { authorization: request.headers.authorization } + : { + "x-aifa-tenant-id": actor.tenantId, + "x-aifa-user-id": actor.userId, + "x-aifa-scopes": actor.scopes.join(","), + }; + const server = createFeatureMcpServer(allowedFor(actor), actorHeaders, options.apiBaseUrl); + const transport = new StreamableHTTPServerTransport({ sessionIdGenerator: undefined }); + try { + await server.connect(transport); + await transport.handleRequest(request, response); + } finally { + await transport.close(); + await server.close(); + } + return true; + }; +} diff --git a/demo/ai-todo-assistant/core/backend/mcp-server.ts b/demo/ai-todo-assistant/core/backend/mcp-server.ts new file mode 100644 index 0000000..1e1163a --- /dev/null +++ b/demo/ai-todo-assistant/core/backend/mcp-server.ts @@ -0,0 +1,88 @@ +import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import { fromJSONSchema } from "zod/v4"; +import { HttpMethod } from "../shared/architecture-enums.js"; +import type { FeatureManifest } from "../shared/feature-manifest.js"; + +export function buildMcpInputSchema(manifest: FeatureManifest): Record { + const mutation = manifest.backend.method !== HttpMethod.Get; + const baseSchema = structuredClone(manifest.backend.contract!.inputSchema); + return mutation + ? { + ...baseSchema, + required: [...((baseSchema.required as string[] | undefined) ?? []), "commandId"], + properties: { + ...((baseSchema.properties as Record | undefined) ?? {}), + commandId: { + type: "string", + minLength: 16, + maxLength: 128, + description: "Stable caller-supplied command identifier; reuse it for retries.", + }, + }, + } + : baseSchema; +} + +export function expandMcpRoute(route: string, input: Record): string { + return route.replace(/:([A-Za-z][A-Za-z0-9_]*)/g, (_match, name: string) => { + const value = input[name]; + if (typeof value !== "string" || !value) throw new Error(`Missing route parameter '${name}'`); + delete input[name]; + return encodeURIComponent(value); + }); +} + +export function createFeatureMcpServer( + manifests: readonly FeatureManifest[], + actorHeaders: Record, + apiBaseUrl: string, + request: typeof fetch = fetch, +): McpServer { + const server = new McpServer({ name: "ai-todo-assistant", version: "0.1.0" }); + for (const manifest of manifests.filter((item) => item.mcp)) { + const mutation = manifest.backend.method !== HttpMethod.Get; + server.registerTool( + manifest.mcp!.toolName, + { + description: manifest.mcp!.description ?? `Run the ${manifest.name} feature.`, + inputSchema: fromJSONSchema(buildMcpInputSchema(manifest)), + annotations: { + readOnlyHint: !mutation, + destructiveHint: manifest.mcp!.requiresConfirmation ?? false, + idempotentHint: true, + }, + }, + async (rawArgs) => { + const input = { ...(rawArgs as Record) }; + const commandId = mutation ? String(input.commandId ?? "") : undefined; + delete input.commandId; + const route = expandMcpRoute(manifest.backend.route, input); + const query = + manifest.backend.method === HttpMethod.Get + ? `?${new URLSearchParams( + Object.entries(input).map(([key, value]) => [key, String(value)]), + ).toString()}` + : ""; + const response = await request(`${apiBaseUrl}${route}${query}`, { + method: manifest.backend.method, + headers: { + "content-type": "application/json", + ...(commandId ? { "idempotency-key": commandId } : {}), + ...actorHeaders, + }, + body: manifest.backend.method === HttpMethod.Get ? undefined : JSON.stringify(input), + }); + const result = (await response.json()) as { + ok: boolean; + value?: unknown; + error?: { message: string }; + }; + return { + content: [{ type: "text", text: JSON.stringify(result.ok ? result.value : result.error) }], + isError: !result.ok, + }; + }, + ); + } + return server; +} diff --git a/demo/ai-todo-assistant/core/backend/mcp.ts b/demo/ai-todo-assistant/core/backend/mcp.ts new file mode 100644 index 0000000..8a0978f --- /dev/null +++ b/demo/ai-todo-assistant/core/backend/mcp.ts @@ -0,0 +1,29 @@ +import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; +import { fileURLToPath } from "node:url"; +import { join } from "node:path"; +import { config, RuntimeEnvironment } from "./config.js"; +import { discoverFeatureManifests } from "./discovery/discover-features.js"; +import { PermissionScope } from "../shared/architecture-enums.js"; +import { createFeatureMcpServer } from "./mcp-server.js"; + +const appRoot = join(fileURLToPath(new URL("../..", import.meta.url))); +const manifests = await discoverFeatureManifests(join(appRoot, "contexts")); +const actorHeaders: Record | undefined = + config.nodeEnv === RuntimeEnvironment.Production + ? config.mcpAccessToken + ? { authorization: `Bearer ${config.mcpAccessToken}` } + : undefined + : config.demoTenantId && config.demoUserId + ? { + "x-aifa-tenant-id": config.demoTenantId, + "x-aifa-user-id": config.demoUserId, + "x-aifa-scopes": Object.values(PermissionScope).join(","), + } + : undefined; +if (!actorHeaders) throw new Error("MCP identity is not configured"); +const server = createFeatureMcpServer( + manifests, + actorHeaders, + `http://${config.host}:${config.port}`, +); +await server.connect(new StdioServerTransport()); diff --git a/demo/ai-todo-assistant/core/backend/outbox/mongo-outbox.ts b/demo/ai-todo-assistant/core/backend/outbox/mongo-outbox.ts new file mode 100644 index 0000000..1965e05 --- /dev/null +++ b/demo/ai-todo-assistant/core/backend/outbox/mongo-outbox.ts @@ -0,0 +1,112 @@ +import { randomUUID } from "node:crypto"; +import { ObjectId, type Db } from "mongodb"; +import { OutboxStatus } from "../../shared/architecture-enums.js"; +import type { OutboxEvent, OutboxStore } from "./outbox-publisher.js"; + +interface MongoOutboxDocument { + _id: ObjectId; + event: { + eventId: string; + eventType: string; + tenantId: string; + occurredAt: string; + [key: string]: unknown; + }; + delivery: { + status: OutboxStatus; + attempts: number; + availableAt: string; + claimToken?: string; + workerId?: string; + leaseExpiresAt?: string; + deliveredAt?: string; + lastError?: string; + }; +} + +export function createMongoOutboxStore( + database: Db, + leaseMs = 60_000, + maximumAttempts = 8, +): OutboxStore { + const events = database.collection("outbox_events"); + return { + async claim(limit, workerId = randomUUID()) { + const claimed: OutboxEvent[] = []; + for (let index = 0; index < limit; index++) { + const now = new Date(); + const claimToken = randomUUID(); + const document = await events.findOneAndUpdate( + { + "delivery.attempts": { $lt: maximumAttempts }, + "delivery.availableAt": { $lte: now.toISOString() }, + $or: [ + { "delivery.status": { $in: [OutboxStatus.Pending, OutboxStatus.Failed] } }, + { + "delivery.status": OutboxStatus.Processing, + "delivery.leaseExpiresAt": { $lte: now.toISOString() }, + }, + ], + }, + { + $set: { + "delivery.status": OutboxStatus.Processing, + "delivery.claimToken": claimToken, + "delivery.workerId": workerId, + "delivery.leaseExpiresAt": new Date(now.getTime() + leaseMs).toISOString(), + }, + $inc: { "delivery.attempts": 1 }, + }, + { sort: { "event.occurredAt": 1 }, returnDocument: "after" }, + ); + if (!document) break; + claimed.push({ + ...document.event, + _id: document._id.toHexString(), + claimToken, + attempts: document.delivery.attempts, + }); + } + return claimed; + }, + async markDelivered(id, claimToken) { + await events.updateOne( + { + _id: typeof id === "string" ? new ObjectId(id) : undefined, + "delivery.status": OutboxStatus.Processing, + "delivery.claimToken": claimToken, + }, + { + $set: { + "delivery.status": OutboxStatus.Delivered, + "delivery.deliveredAt": new Date().toISOString(), + }, + $unset: { + "delivery.lastError": "", + "delivery.leaseExpiresAt": "", + "delivery.claimToken": "", + }, + }, + ); + }, + async markFailed(id, claimToken, message, attempts) { + const dead = attempts >= maximumAttempts; + const backoffMs = Math.min(300_000, 1_000 * 2 ** Math.max(0, attempts - 1)); + await events.updateOne( + { + _id: typeof id === "string" ? new ObjectId(id) : undefined, + "delivery.status": OutboxStatus.Processing, + "delivery.claimToken": claimToken, + }, + { + $set: { + "delivery.status": dead ? OutboxStatus.DeadLetter : OutboxStatus.Failed, + "delivery.lastError": message.slice(0, 2_000), + "delivery.availableAt": new Date(Date.now() + backoffMs).toISOString(), + }, + $unset: { "delivery.leaseExpiresAt": "", "delivery.claimToken": "" }, + }, + ); + }, + }; +} diff --git a/demo/ai-todo-assistant/core/backend/outbox/outbox-publisher.ts b/demo/ai-todo-assistant/core/backend/outbox/outbox-publisher.ts new file mode 100644 index 0000000..ed1c9fd --- /dev/null +++ b/demo/ai-todo-assistant/core/backend/outbox/outbox-publisher.ts @@ -0,0 +1,38 @@ +export interface OutboxEvent { + _id: string | number; + claimToken: string; + eventId: string; + eventType: string; + tenantId: string; + occurredAt: string; + attempts: number; + [key: string]: unknown; +} +export interface OutboxStore { + claim(limit: number, workerId?: string): Promise; + markDelivered(id: unknown, claimToken: string): Promise; + markFailed(id: unknown, claimToken: string, message: string, attempts: number): Promise; +} +export type EventHandler = (event: OutboxEvent) => Promise; +export async function publishPendingEvents( + store: OutboxStore, + handler: EventHandler, + limit = 50, + workerId = crypto.randomUUID(), +): Promise { + const events = await store.claim(limit, workerId); + for (const event of events) { + try { + await handler(event); + await store.markDelivered(event._id, event.claimToken); + } catch (cause) { + await store.markFailed( + event._id, + event.claimToken, + cause instanceof Error ? cause.message : "Unknown delivery error", + event.attempts, + ); + } + } + return events.length; +} diff --git a/demo/ai-todo-assistant/core/backend/outbox/worker.ts b/demo/ai-todo-assistant/core/backend/outbox/worker.ts new file mode 100644 index 0000000..0b96120 --- /dev/null +++ b/demo/ai-todo-assistant/core/backend/outbox/worker.ts @@ -0,0 +1,17 @@ +import { config } from "../config.js"; +import { createMongoDatabase } from "../database/mongo.js"; +import { createMongoOutboxStore } from "./mongo-outbox.js"; +import { publishPendingEvents } from "./outbox-publisher.js"; +if (!config.mongoUri) throw new Error("MONGODB_URI is required for the outbox worker"); +const database = createMongoDatabase(config.mongoUri, config.mongoDatabase); +const store = createMongoOutboxStore(await database.connect()); +let running = true; +process.on("SIGTERM", () => (running = false)); +process.on("SIGINT", () => (running = false)); +while (running) { + const delivered = await publishPendingEvents(store, async (event) => { + console.log(JSON.stringify({ eventId: event.eventId, eventType: event.eventType })); + }); + if (!delivered) await new Promise((resolve) => setTimeout(resolve, 1_000)); +} +await database.close(); diff --git a/demo/ai-todo-assistant/core/backend/runtime/aifa-runtime.ts b/demo/ai-todo-assistant/core/backend/runtime/aifa-runtime.ts new file mode 100644 index 0000000..837ad0b --- /dev/null +++ b/demo/ai-todo-assistant/core/backend/runtime/aifa-runtime.ts @@ -0,0 +1,72 @@ +import { + fail, + ok, + type AifaFeature, + type Actor, + type FeatureResult, + type RequestMetadata, +} from "../../shared/aifa.js"; +import { CapabilityName, ErrorCode } from "../../shared/architecture-enums.js"; + +export function createAifaRuntime>( + allCapabilities: Capabilities, +) { + class CapabilityAccessError extends Error { + constructor( + readonly code: ErrorCode.CapabilityMissing | ErrorCode.CapabilityNotAllowed, + message: string, + ) { + super(message); + } + } + + function bindCapabilities( + allowedNames: readonly CapabilityName[], + ): FeatureCapabilities { + const allowed = new Set(allowedNames); + return new Proxy(allCapabilities, { + get(target, name: string | symbol) { + if (typeof name !== "string") return undefined; + if (!allowed.has(name as CapabilityName)) + return async () => { + throw new CapabilityAccessError( + ErrorCode.CapabilityNotAllowed, + `Capability '${name}' is not declared for this feature`, + ); + }; + const capability = target[name]; + return capability; + }, + }) as unknown as FeatureCapabilities; + } + + async function run( + feature: AifaFeature, + input: Input, + actor: Actor, + metadata: RequestMetadata, + ): Promise> { + const missing = feature.capabilities.filter( + (name) => typeof allCapabilities[name] !== "function", + ); + if (missing.length) + return fail(ErrorCode.CapabilityMissing, "Required capabilities are unavailable", { + capabilities: missing, + }); + try { + return await feature.execute({ + input, + actor, + metadata, + capabilities: bindCapabilities(feature.capabilities), + ok, + fail, + }); + } catch (cause) { + if (cause instanceof CapabilityAccessError) return fail(cause.code, cause.message); + throw cause; + } + } + + return { run }; +} diff --git a/demo/ai-todo-assistant/core/backend/runtime/mongo-platform-capabilities.ts b/demo/ai-todo-assistant/core/backend/runtime/mongo-platform-capabilities.ts new file mode 100644 index 0000000..d06bb67 --- /dev/null +++ b/demo/ai-todo-assistant/core/backend/runtime/mongo-platform-capabilities.ts @@ -0,0 +1,66 @@ +import { randomUUID } from "node:crypto"; +import type { ClientSession, Db } from "mongodb"; +import type { Actor, PlatformCapabilities, RequestMetadata } from "../../shared/aifa.js"; +import { OutboxStatus } from "../../shared/architecture-enums.js"; + +export function createMongoPlatformCapabilities( + database: Db, + actor: Actor, + metadata: RequestMetadata, + session?: ClientSession, + validateEvent?: (value: unknown) => { path: string; message: string }[], +): PlatformCapabilities { + const audit = database.collection("audit_events"); + const outbox = database.collection("outbox_events"); + return { + async IdCreate() { + return randomUUID(); + }, + async ClockNow() { + return new Date().toISOString(); + }, + async DomainEventEmit({ eventType, data }) { + if (!metadata.idempotencyKey) + throw new Error(`Mutation event '${eventType}' requires command metadata`); + const event = { + eventId: randomUUID(), + eventType, + schemaVersion: 1, + tenantId: actor.tenantId, + actorId: actor.userId, + correlationId: metadata.correlationId, + causationId: metadata.causationId, + idempotencyKey: metadata.idempotencyKey, + ...data, + occurredAt: new Date().toISOString(), + }; + const errors = validateEvent?.(event) ?? []; + if (errors.length) + throw new Error(`Domain event '${eventType}' violated its contract: ${JSON.stringify(errors)}`); + await audit.insertOne({ ...event }, { session }); + await outbox.insertOne( + { + event, + delivery: { + status: OutboxStatus.Pending, + attempts: 0, + availableAt: event.occurredAt, + }, + }, + { session }, + ); + }, + }; +} + +export async function ensurePlatformIndexes(database: Db): Promise { + await Promise.all([ + database + .collection("audit_events") + .createIndex({ tenantId: 1, occurredAt: -1 }, { name: "tenant_audit_time" }), + database.collection("outbox_events").createIndex( + { "delivery.status": 1, "delivery.availableAt": 1, "event.occurredAt": 1 }, + { name: "outbox_delivery" }, + ), + ]); +} diff --git a/demo/ai-todo-assistant/core/backend/server.ts b/demo/ai-todo-assistant/core/backend/server.ts new file mode 100644 index 0000000..fe82346 --- /dev/null +++ b/demo/ai-todo-assistant/core/backend/server.ts @@ -0,0 +1,217 @@ +import { createServer } from "node:http"; +import { fileURLToPath } from "node:url"; +import { join } from "node:path"; +import type { ClientSession } from "mongodb"; +import { config } from "./config.js"; +import { createMongoDatabase } from "./database/mongo.js"; +import { createAifaRuntime } from "./runtime/aifa-runtime.js"; +import { createMongoPlatformCapabilities, ensurePlatformIndexes } from "./runtime/mongo-platform-capabilities.js"; +import { discoverFeatureManifests } from "./discovery/discover-features.js"; +import { discoverContextInfrastructure } from "./discovery/context-infrastructure.js"; +import { resolveActor } from "./auth/actor-resolver.js"; +import { createMongoIdempotencyStore } from "./idempotency/mongo-idempotency.js"; +import { createCanonicalFingerprint } from "./idempotency/canonical-fingerprint.js"; +import { createRequestHandler } from "./http/request-handler.js"; +import { ErrorCode, HttpMethod } from "../shared/architecture-enums.js"; +import type { Actor, RequestMetadata } from "../shared/aifa.js"; +import type { FeatureManifest } from "../shared/feature-manifest.js"; +import { createRemoteMcpHandler } from "./mcp-http.js"; + +const memoryServer = config.mongoUri + ? undefined + : await (await import("mongodb-memory-server")).MongoMemoryReplSet.create({ + replSet: { count: 1 }, + }); +const database = createMongoDatabase( + config.mongoUri ?? memoryServer!.getUri(), + config.mongoDatabase, +); +const appRoot = join(fileURLToPath(new URL("../..", import.meta.url))); +const contextsRoot = join(appRoot, "contexts"); +const [manifests, infrastructure] = await Promise.all([ + discoverFeatureManifests(contextsRoot), + discoverContextInfrastructure(contextsRoot), +]); +const mongoDatabase = await database.connect(); +await Promise.all([ + ensurePlatformIndexes(mongoDatabase), + ...infrastructure.map((adapter) => adapter.ensureIndexes(mongoDatabase)), +]); +const idempotency = createMongoIdempotencyStore(mongoDatabase, config.idempotencyLeaseMs); +await idempotency.ensureIndexes(); +const infrastructureSettings = { + ollamaBaseUrl: config.ollamaBaseUrl, + ollamaModel: config.ollamaModel, +}; + +function createCapabilities( + manifest: FeatureManifest, + actor: Actor, + metadata: RequestMetadata, + session?: ClientSession, + overrides: Record = {}, +) { + return { + ...createMongoPlatformCapabilities( + mongoDatabase, + actor, + metadata, + session, + manifest.backend.contract?.validateEvent, + ), + ...Object.assign( + {}, + ...infrastructure.map((adapter) => + adapter.createCapabilities({ + database: mongoDatabase, + actor, + metadata, + session, + settings: infrastructureSettings, + }), + ), + ), + ...overrides, + }; +} + +async function prepareExternalCapabilities( + manifest: FeatureManifest, + input: Record, + actor: Actor, + metadata: RequestMetadata, + durableCommand: { + record: { + tenantId: string; + actorId: string; + featureName: FeatureManifest["name"]; + idempotencyKey: string; + }; + executionId: string; + preparedCapabilities: Record; + }, +): Promise> { + const external = new Set(infrastructure.flatMap(({ externalCapabilities = [] }) => externalCapabilities)); + const capabilities = createCapabilities(manifest, actor, metadata); + const prepared: Record = {}; + for (const name of manifest.backend.feature.capabilities.filter((item) => external.has(item))) { + if (name in durableCommand.preparedCapabilities) { + const durableValue = durableCommand.preparedCapabilities[name]; + prepared[name] = async () => durableValue; + continue; + } + const capability = capabilities[name]; + if (typeof capability !== "function") continue; + await idempotency.heartbeat(durableCommand.record, durableCommand.executionId); + const result = await (capability as (value: unknown) => Promise)(input); + await idempotency.savePreparedCapability( + durableCommand.record, + durableCommand.executionId, + name, + result, + ); + prepared[name] = async () => result; + } + return prepared; +} + +const apiHandler = createRequestHandler({ + manifests, + corsOrigin: config.corsOrigin, + resolveActor, + readiness: async () => { + await mongoDatabase.command({ ping: 1 }); + return { + mongo: "ok", + ...Object.fromEntries( + infrastructure.flatMap((adapter) => + adapter.health ? [[adapter.name, adapter.health()]] : [], + ), + ), + }; + }, + requestTimeoutMs: config.requestTimeoutMs, + async run(manifest, input, actor, metadata) { + if (manifest.backend.method === HttpMethod.Get) + return createAifaRuntime(createCapabilities(manifest, actor, metadata)).run( + manifest.backend.feature, + input, + actor, + metadata, + ); + const idempotencyKey = metadata.idempotencyKey!; + const record = { + tenantId: actor.tenantId, + actorId: actor.userId, + featureName: manifest.name, + idempotencyKey, + }; + const claim = await idempotency.claim({ + ...record, + inputFingerprint: createCanonicalFingerprint(input), + commandInput: input, + }); + if (claim.kind === "completed") return claim.result; + if (claim.kind === "key-reused") + return { + ok: false, + error: { + code: ErrorCode.IdempotencyKeyReused, + message: "The idempotency key was used with different input", + details: {}, + }, + }; + if (claim.kind === "in-progress") + return { + ok: false, + error: { + code: ErrorCode.RequestInProgress, + message: "A request with this idempotency key is still running", + details: {}, + }, + }; + + const prepared = await prepareExternalCapabilities(manifest, input, actor, metadata, { + record, + executionId: claim.executionId, + preparedCapabilities: claim.preparedCapabilities, + }); + return database.withTransaction(async (session) => { + const result = await createAifaRuntime( + createCapabilities(manifest, actor, metadata, session, prepared), + ).run(manifest.backend.feature, input, actor, metadata); + await idempotency.complete(record, claim.executionId, result, session); + return result; + }); + }, +}); +const remoteMcpHandler = createRemoteMcpHandler({ + manifests, + resolveActor, + apiBaseUrl: `http://${config.host}:${config.port}`, + resourceUrl: new URL("/mcp", config.publicBaseUrl).href, + authorizationServer: config.oidcIssuerUrl, +}); +const server = createServer(async (request, response) => { + if (await remoteMcpHandler(request, response)) return; + await apiHandler(request, response); +}); + +server.listen(config.port, config.host, () => { + console.log(JSON.stringify({ + timestamp: new Date().toISOString(), + level: "info", + message: "server_listening", + address: `http://${config.host}:${config.port}`, + })); +}); +let closing = false; +async function shutdown(): Promise { + if (closing) return; + closing = true; + await new Promise((resolve) => server.close(() => resolve())); + await database.close(); + await memoryServer?.stop(); +} +process.on("SIGTERM", () => void shutdown()); +process.on("SIGINT", () => void shutdown()); diff --git a/demo/ai-todo-assistant/core/backend/telemetry/logger.ts b/demo/ai-todo-assistant/core/backend/telemetry/logger.ts new file mode 100644 index 0000000..35c0698 --- /dev/null +++ b/demo/ai-todo-assistant/core/backend/telemetry/logger.ts @@ -0,0 +1,16 @@ +export interface LogFields { + correlationId?: string; + feature?: string; + durationMs?: number; + status?: number; + error?: string; + [key: string]: unknown; +} +function write(level: "info" | "error", message: string, fields: LogFields = {}): void { + const entry = JSON.stringify({ timestamp: new Date().toISOString(), level, message, ...fields }); + (level === "error" ? console.error : console.log)(entry); +} +export const logger = { + info: (message: string, fields?: LogFields) => write("info", message, fields), + error: (message: string, fields?: LogFields) => write("error", message, fields), +}; diff --git a/demo/ai-todo-assistant/core/frontend/App.tsx b/demo/ai-todo-assistant/core/frontend/App.tsx new file mode 100644 index 0000000..a6387bb --- /dev/null +++ b/demo/ai-todo-assistant/core/frontend/App.tsx @@ -0,0 +1,152 @@ +import ChevronLeft from "@mui/icons-material/ChevronLeft"; +import Menu from "@mui/icons-material/Menu"; +import { + AppBar, + Box, + Drawer, + IconButton, + Stack, + Toolbar, + useMediaQuery, + useTheme, +} from "@mui/material"; +import { useEffect, useMemo, useRef, useState } from "react"; +import { AppSurface, SlotName } from "../shared/architecture-enums.js"; +import type { ShellModel } from "./app-model.js"; +import { LazySlot } from "./slots/LazySlot.js"; +import { beginSignIn, completeSignIn, getAccessToken } from "./auth/oidc.js"; +import { visualTokens } from "./theme.js"; +import { useQueryClient } from "./query/react-query.js"; + +const drawerWidth = 248; + +export function App() { + const [activeSurface, setActiveSurface] = useState(AppSurface.Landing); + const [signedIn, setSignedIn] = useState(import.meta.env.DEV || Boolean(getAccessToken())); + const [mobileNavOpen, setMobileNavOpen] = useState(false); + const queryClient = useQueryClient(); + const previousIdentityState = useRef(signedIn); + const theme = useTheme(); + const isDesktop = useMediaQuery(theme.breakpoints.up("md")); + useEffect(() => { + if (import.meta.env.DEV) return; + void completeSignIn().then((authenticated) => { + setSignedIn(authenticated); + if (authenticated) setActiveSurface(AppSurface.Dashboard); + }); + }, []); + useEffect(() => { + if (previousIdentityState.current === signedIn) return; + previousIdentityState.current = signedIn; + queryClient.clear(); + }, [queryClient, signedIn]); + const model = useMemo( + () => ({ + activeSurface, + signedIn, + signIn: async () => { + if (import.meta.env.DEV) { + setActiveSurface(AppSurface.Dashboard); + return; + } + await beginSignIn(); + }, + setActiveSurface: (surface) => { + setActiveSurface(surface); + setMobileNavOpen(false); + }, + }), + [activeSurface, signedIn], + ); + const navigation = ; + const isLanding = activeSurface === AppSurface.Landing; + return ( + + value.zIndex.drawer + 1, + }} + > + + {!isLanding && !isDesktop && ( + setMobileNavOpen(true)} + aria-label="open navigation" + > + + + )} + + + + + + {!isLanding && ( + + + + {navigation} + + setMobileNavOpen(false)} + ModalProps={{ keepMounted: true }} + sx={{ + display: { md: "none" }, + "& .MuiDrawer-paper": { width: drawerWidth, bgcolor: visualTokens.ink, color: "white" }, + }} + > + + setMobileNavOpen(false)} aria-label="close navigation"> + + + + {navigation} + + + )} + + + + + + ); +} diff --git a/demo/ai-todo-assistant/core/frontend/FrontendRuntimeProvider.tsx b/demo/ai-todo-assistant/core/frontend/FrontendRuntimeProvider.tsx new file mode 100644 index 0000000..973a9c9 --- /dev/null +++ b/demo/ai-todo-assistant/core/frontend/FrontendRuntimeProvider.tsx @@ -0,0 +1,12 @@ +import type { ReactNode } from "react"; +import { FrontendEventProvider } from "./events/FrontendEventProvider.js"; +import { queryClient } from "./query/query-client.js"; +import { QueryClientProvider } from "./query/react-query.js"; + +export function FrontendRuntimeProvider({ children }: { children: ReactNode }) { + return ( + + {children} + + ); +} diff --git a/demo/ai-todo-assistant/core/frontend/api.ts b/demo/ai-todo-assistant/core/frontend/api.ts new file mode 100644 index 0000000..efda6ae --- /dev/null +++ b/demo/ai-todo-assistant/core/frontend/api.ts @@ -0,0 +1,30 @@ +import { getAccessToken } from "./auth/oidc.js"; + +export async function api(path: string, options: RequestInit = {}): Promise { + const developmentHeaders: Record = import.meta.env.DEV + ? { + "x-aifa-tenant-id": "local-tenant", + "x-aifa-user-id": "local-user", + "x-aifa-scopes": + "TaskRead,TaskWrite,TaskDelete,AssistantPlanGenerate,SettingsRead", + } + : {}; + const response = await fetch(`${import.meta.env.VITE_API_BASE_URL ?? ""}${path}`, { + ...options, + headers: { + "content-type": "application/json", + ...developmentHeaders, + ...(!import.meta.env.DEV && getAccessToken() + ? { authorization: `Bearer ${getAccessToken()}` } + : {}), + ...options.headers, + }, + }); + const result = (await response.json()) as { + ok: boolean; + value?: Value; + error?: { message: string }; + }; + if (!response.ok || !result.ok) throw new Error(result.error?.message ?? "Request failed"); + return result.value as Value; +} diff --git a/demo/ai-todo-assistant/core/frontend/app-model.ts b/demo/ai-todo-assistant/core/frontend/app-model.ts new file mode 100644 index 0000000..b7781f5 --- /dev/null +++ b/demo/ai-todo-assistant/core/frontend/app-model.ts @@ -0,0 +1,32 @@ +import type { AppSurface } from "../shared/architecture-enums.js"; +import type { + TaskManagementTaxonomyV1, + TaskViewV1, +} from "../shared/generated/contracts.js"; + +export type TaskCategory = TaskManagementTaxonomyV1["category"]; +export type TaskPriority = TaskManagementTaxonomyV1["priority"]; +export type TaskStatus = TaskManagementTaxonomyV1["status"]; +export type TaskView = TaskViewV1; + +export interface ShellModel { + activeSurface: AppSurface; + signedIn: boolean; + signIn(): Promise; + setActiveSurface(value: AppSurface): void; +} + +export type EmptySlotModel = Readonly>; + +export interface TaskRowActionsModel { + task: TaskView; +} + +export interface TaskListModel { + tasks: readonly TaskView[]; + error: string; + categoryFilter: TaskCategory | ""; + statusFilter: TaskStatus | ""; + setCategoryFilter(value: TaskCategory | ""): void; + setStatusFilter(value: TaskStatus | ""): void; +} diff --git a/demo/ai-todo-assistant/core/frontend/auth/oidc.ts b/demo/ai-todo-assistant/core/frontend/auth/oidc.ts new file mode 100644 index 0000000..e6d2408 --- /dev/null +++ b/demo/ai-todo-assistant/core/frontend/auth/oidc.ts @@ -0,0 +1,109 @@ +interface OidcMetadata { + authorization_endpoint: string; + token_endpoint: string; +} +const accessTokenKey = "aifa.oidc.access-token"; +const verifierKey = "aifa.oidc.pkce-verifier"; +const stateKey = "aifa.oidc.state"; +let metadataPromise: Promise | undefined; + +function configuration() { + return { + issuer: import.meta.env.VITE_OIDC_ISSUER_URL as string | undefined, + clientId: import.meta.env.VITE_OIDC_CLIENT_ID as string | undefined, + audience: import.meta.env.VITE_OIDC_AUDIENCE as string | undefined, + redirectUri: + (import.meta.env.VITE_OIDC_REDIRECT_URI as string | undefined) ?? + `${window.location.origin}${window.location.pathname}`, + }; +} +async function metadata(): Promise { + const { issuer } = configuration(); + if (!issuer) throw new Error("OIDC is not configured for this browser build"); + metadataPromise ??= fetch( + `${issuer.replace(/\/$/, "")}/.well-known/openid-configuration`, + ).then(async (response) => { + if (!response.ok) throw new Error("Unable to load OIDC metadata"); + return (await response.json()) as OidcMetadata; + }); + return metadataPromise; +} +function randomBase64Url(bytes = 32): string { + const value = crypto.getRandomValues(new Uint8Array(bytes)); + return btoa(String.fromCharCode(...value)) + .replaceAll("+", "-") + .replaceAll("/", "_") + .replaceAll("=", ""); +} +async function sha256Base64Url(value: string): Promise { + const digest = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(value)); + return btoa(String.fromCharCode(...new Uint8Array(digest))) + .replaceAll("+", "-") + .replaceAll("/", "_") + .replaceAll("=", ""); +} + +export function getAccessToken(): string | undefined { + return window.sessionStorage.getItem(accessTokenKey) ?? undefined; +} +export function isOidcConfigured(): boolean { + const { issuer, clientId } = configuration(); + return Boolean(issuer && clientId); +} +export async function beginSignIn(): Promise { + const config = configuration(); + if (!config.clientId) throw new Error("OIDC client ID is not configured"); + const [provider, verifier, state] = await Promise.all([ + metadata(), + Promise.resolve(randomBase64Url(64)), + Promise.resolve(randomBase64Url()), + ]); + sessionStorage.setItem(verifierKey, verifier); + sessionStorage.setItem(stateKey, state); + const authorization = new URL(provider.authorization_endpoint); + authorization.search = new URLSearchParams({ + client_id: config.clientId, + redirect_uri: config.redirectUri, + response_type: "code", + scope: "openid profile offline_access", + state, + code_challenge: await sha256Base64Url(verifier), + code_challenge_method: "S256", + ...(config.audience ? { audience: config.audience } : {}), + }).toString(); + window.location.assign(authorization); +} +export async function completeSignIn(): Promise { + const callback = new URL(window.location.href); + const code = callback.searchParams.get("code"); + if (!code) return Boolean(getAccessToken()); + const expectedState = sessionStorage.getItem(stateKey); + if (!expectedState || callback.searchParams.get("state") !== expectedState) + throw new Error("OIDC callback state did not match"); + const verifier = sessionStorage.getItem(verifierKey); + const config = configuration(); + if (!verifier || !config.clientId) throw new Error("OIDC PKCE state is missing"); + const provider = await metadata(); + const response = await fetch(provider.token_endpoint, { + method: "POST", + headers: { "content-type": "application/x-www-form-urlencoded" }, + body: new URLSearchParams({ + grant_type: "authorization_code", + client_id: config.clientId, + redirect_uri: config.redirectUri, + code, + code_verifier: verifier, + }), + }); + if (!response.ok) throw new Error("OIDC code exchange failed"); + const tokens = (await response.json()) as { access_token?: string }; + if (!tokens.access_token) throw new Error("OIDC response did not include an access token"); + sessionStorage.setItem(accessTokenKey, tokens.access_token); + sessionStorage.removeItem(verifierKey); + sessionStorage.removeItem(stateKey); + callback.searchParams.delete("code"); + callback.searchParams.delete("state"); + callback.searchParams.delete("session_state"); + window.history.replaceState({}, document.title, callback); + return true; +} diff --git a/demo/ai-todo-assistant/core/frontend/discovery/discover-contributions.ts b/demo/ai-todo-assistant/core/frontend/discovery/discover-contributions.ts new file mode 100644 index 0000000..20c5cd2 --- /dev/null +++ b/demo/ai-todo-assistant/core/frontend/discovery/discover-contributions.ts @@ -0,0 +1,54 @@ +import { frontendModules } from "../../../composition/generated/frontend-modules.js"; +import { + frontendEventRegistry, + type FrontendEventConsumer, +} from "../events/event-registry.js"; +import { registerSlotContribution, type AnySlotContribution } from "../slots/slot-registry.js"; + +interface ContributionModule { + contribution?: AnySlotContribution; + contributions?: AnySlotContribution[]; + eventConsumers?: FrontendEventConsumer[]; +} +const loaded = new Set(); +const pending = new Map>(); + +interface EventConsumerDeclaration { + readonly name: string; + readonly eventType: string; + readonly contract: string; +} + +async function loadModule( + id: string, + declarations: readonly EventConsumerDeclaration[], + load: () => Promise, +): Promise { + if (loaded.has(id)) return; + const existing = pending.get(id); + if (existing) return existing; + const operation = load().then((value) => { + const module = value as ContributionModule; + const declared = declarations + .map(({ name, eventType, contract }) => `${name}:${eventType}:${contract}`) + .sort(); + const implemented = (module.eventConsumers ?? []) + .map(({ name, eventType, contract }) => `${name}:${eventType}:${contract}`) + .sort(); + if (declared.join() !== implemented.join()) + throw new Error(`Frontend event consumers do not match feature definition '${id}'`); + if (module.contribution) registerSlotContribution(module.contribution); + module.contributions?.forEach(registerSlotContribution); + module.eventConsumers?.forEach((consumer) => frontendEventRegistry.register(consumer)); + loaded.add(id); + pending.delete(id); + }); + pending.set(id, operation); + return operation; +} + +export async function ensureFrontendContributions(): Promise { + await Promise.all( + frontendModules.map(({ id, eventConsumers, load }) => loadModule(id, eventConsumers, load)), + ); +} diff --git a/demo/ai-todo-assistant/core/frontend/events/FrontendEventProvider.tsx b/demo/ai-todo-assistant/core/frontend/events/FrontendEventProvider.tsx new file mode 100644 index 0000000..87af08b --- /dev/null +++ b/demo/ai-todo-assistant/core/frontend/events/FrontendEventProvider.tsx @@ -0,0 +1,22 @@ +import { useEffect, type ReactNode } from "react"; +import { frontendEventRegistry, parseFrontendDomainEvent } from "./event-registry.js"; +import { useQueryClient } from "../query/react-query.js"; + +export function FrontendEventProvider({ children }: { children: ReactNode }) { + const queryClient = useQueryClient(); + useEffect(() => { + const streamUrl = import.meta.env.VITE_EVENT_STREAM_URL; + if (!streamUrl) return; + const source = new EventSource(streamUrl, { withCredentials: true }); + source.onmessage = (message) => { + try { + const event = parseFrontendDomainEvent(JSON.parse(message.data) as unknown); + void frontendEventRegistry.dispatch(event, { queryClient }); + } catch (cause) { + console.error("Rejected frontend domain event", cause); + } + }; + return () => source.close(); + }, [queryClient]); + return children; +} diff --git a/demo/ai-todo-assistant/core/frontend/events/event-registry.ts b/demo/ai-todo-assistant/core/frontend/events/event-registry.ts new file mode 100644 index 0000000..f0f25b9 --- /dev/null +++ b/demo/ai-todo-assistant/core/frontend/events/event-registry.ts @@ -0,0 +1,59 @@ +import type { QueryClient } from "../query/query-client.js"; + +export interface FrontendDomainEvent { + eventType: string; + schemaVersion: number; + tenantId: string; + [property: string]: unknown; +} + +export interface FrontendEventContext { + queryClient: QueryClient; +} + +export interface FrontendEventConsumer { + name: string; + eventType: string; + contract: string; + accepts(value: FrontendDomainEvent): value is Event; + handle(event: Event, context: FrontendEventContext): void | Promise; +} + +export class FrontendEventRegistry { + readonly #consumers = new Map(); + + register(consumer: FrontendEventConsumer): void { + const existing = this.#consumers.get(consumer.eventType) ?? []; + if (existing.some(({ name }) => name === consumer.name)) + throw new Error(`Duplicate frontend event consumer '${consumer.eventType}/${consumer.name}'`); + this.#consumers.set(consumer.eventType, [...existing, consumer]); + } + + async dispatch(event: FrontendDomainEvent, context: FrontendEventContext): Promise { + const consumers = this.#consumers.get(event.eventType) ?? []; + const eventType = event.eventType; + await Promise.all( + consumers.map(async (consumer) => { + if (!consumer.accepts(event)) + throw new Error(`Frontend event '${eventType}' failed consumer validation`); + await consumer.handle(event, context); + }), + ); + } + + clear(): void { + this.#consumers.clear(); + } +} + +export const frontendEventRegistry = new FrontendEventRegistry(); + +export function parseFrontendDomainEvent(value: unknown): FrontendDomainEvent { + if (!value || typeof value !== "object") throw new Error("Frontend event must be an object"); + const candidate = value as Record; + if (typeof candidate.eventType !== "string") throw new Error("Frontend eventType is required"); + if (typeof candidate.schemaVersion !== "number") + throw new Error("Frontend event schemaVersion is required"); + if (typeof candidate.tenantId !== "string") throw new Error("Frontend event tenantId is required"); + return candidate as FrontendDomainEvent; +} diff --git a/demo/ai-todo-assistant/core/frontend/main.tsx b/demo/ai-todo-assistant/core/frontend/main.tsx new file mode 100644 index 0000000..1fd687e --- /dev/null +++ b/demo/ai-todo-assistant/core/frontend/main.tsx @@ -0,0 +1,16 @@ +import { CssBaseline, ThemeProvider } from "@mui/material"; +import { createRoot } from "react-dom/client"; +import { App } from "./App.js"; +import { FrontendRuntimeProvider } from "./FrontendRuntimeProvider.js"; +import { theme } from "./theme.js"; +const root = document.querySelector("#root"); +if (!root) throw new Error("React root was not found"); + +createRoot(root).render( + + + + + + , +); diff --git a/demo/ai-todo-assistant/core/frontend/query/query-client.ts b/demo/ai-todo-assistant/core/frontend/query/query-client.ts new file mode 100644 index 0000000..c07fbde --- /dev/null +++ b/demo/ai-todo-assistant/core/frontend/query/query-client.ts @@ -0,0 +1,169 @@ +export type QueryKeyPart = string | number | boolean | null; +export type QueryKey = readonly QueryKeyPart[]; +export type QueryTag = string; + +export type QueryStatus = "idle" | "pending" | "success" | "error"; + +export interface QuerySnapshot { + status: QueryStatus; + data: Value | undefined; + error: Error | undefined; + isFetching: boolean; +} + +export interface QueryFunctionContext { + signal: AbortSignal; +} + +export interface QueryOptions { + key: QueryKey; + tags: readonly QueryTag[]; + queryFn(context: QueryFunctionContext): Promise; +} + +interface QueryEntry { + key: QueryKey; + tags: Set; + queryFn(context: QueryFunctionContext): Promise; + snapshot: QuerySnapshot; + stale: boolean; + generation: number; + controller?: AbortController; + promise?: Promise; + listeners: Set<() => void>; +} + +const idleSnapshot: QuerySnapshot = { + status: "idle", + data: undefined, + error: undefined, + isFetching: false, +}; + +export function hashQueryKey(key: QueryKey): string { + return JSON.stringify(key); +} + +function toError(cause: unknown): Error { + return cause instanceof Error ? cause : new Error("Query failed"); +} + +function isAbortError(cause: unknown): boolean { + return cause instanceof Error && cause.name === "AbortError"; +} + +export class QueryClient { + readonly #entries = new Map(); + + configure(options: QueryOptions): string { + const hash = hashQueryKey(options.key); + const current = this.#entries.get(hash) as QueryEntry | undefined; + if (current) { + current.queryFn = options.queryFn; + current.tags = new Set(options.tags); + return hash; + } + this.#entries.set(hash, { + key: options.key, + tags: new Set(options.tags), + queryFn: options.queryFn, + snapshot: idleSnapshot, + stale: true, + generation: 0, + listeners: new Set(), + }); + return hash; + } + + getSnapshot(hash: string): QuerySnapshot { + return (this.#entries.get(hash)?.snapshot ?? idleSnapshot) as QuerySnapshot; + } + + subscribe(hash: string, listener: () => void): () => void { + const entry = this.#requireEntry(hash); + entry.listeners.add(listener); + if (entry.stale || entry.snapshot.status === "idle") void this.#execute(hash); + return () => entry.listeners.delete(listener); + } + + refetch(hash: string): Promise { + return this.#execute(hash, true); + } + + async invalidateTags(tags: readonly QueryTag[]): Promise { + const targets = new Set(tags); + const refetches: Promise[] = []; + for (const [hash, entry] of this.#entries) { + if (![...entry.tags].some((tag) => targets.has(tag))) continue; + entry.stale = true; + if (entry.listeners.size > 0) refetches.push(this.#execute(hash)); + } + await Promise.all(refetches); + } + + clear(): void { + for (const [hash, entry] of this.#entries) { + entry.controller?.abort(); + entry.generation += 1; + entry.stale = true; + entry.promise = undefined; + entry.snapshot = idleSnapshot; + this.#notify(entry); + if (entry.listeners.size > 0) void this.#execute(hash); + } + } + + #requireEntry(hash: string): QueryEntry { + const entry = this.#entries.get(hash); + if (!entry) throw new Error(`Query '${hash}' is not configured`); + return entry; + } + + #notify(entry: QueryEntry): void { + entry.listeners.forEach((listener) => listener()); + } + + #execute(hash: string, force = false): Promise { + const entry = this.#requireEntry(hash); + if (entry.promise && !force) return entry.promise; + if (force) entry.controller?.abort(); + const generation = ++entry.generation; + const controller = new AbortController(); + entry.controller = controller; + entry.stale = false; + entry.snapshot = { + status: entry.snapshot.data === undefined ? "pending" : entry.snapshot.status, + data: entry.snapshot.data, + error: undefined, + isFetching: true, + }; + this.#notify(entry); + const operation = entry + .queryFn({ signal: controller.signal }) + .then((data) => { + if (entry.generation !== generation) return; + entry.snapshot = { status: "success", data, error: undefined, isFetching: false }; + this.#notify(entry); + }) + .catch((cause: unknown) => { + if (entry.generation !== generation || isAbortError(cause)) return; + entry.snapshot = { + status: "error", + data: entry.snapshot.data, + error: toError(cause), + isFetching: false, + }; + this.#notify(entry); + }) + .finally(() => { + if (entry.generation === generation) { + entry.promise = undefined; + entry.controller = undefined; + } + }); + entry.promise = operation; + return operation; + } +} + +export const queryClient = new QueryClient(); diff --git a/demo/ai-todo-assistant/core/frontend/query/react-query.tsx b/demo/ai-todo-assistant/core/frontend/query/react-query.tsx new file mode 100644 index 0000000..e3ebe0a --- /dev/null +++ b/demo/ai-todo-assistant/core/frontend/query/react-query.tsx @@ -0,0 +1,95 @@ +import { + createContext, + useCallback, + useContext, + useMemo, + useState, + useSyncExternalStore, + type ReactNode, +} from "react"; +import { + QueryClient, + hashQueryKey, + type QueryOptions, + type QuerySnapshot, +} from "./query-client.js"; + +const QueryClientContext = createContext(null); + +export function QueryClientProvider({ + client, + children, +}: { + client: QueryClient; + children: ReactNode; +}) { + return {children}; +} + +export function useQueryClient(): QueryClient { + const client = useContext(QueryClientContext); + if (!client) throw new Error("QueryClientProvider is not registered"); + return client; +} + +export interface UseQueryResult extends QuerySnapshot { + refetch(): Promise; +} + +export function useQuery(options: QueryOptions): UseQueryResult { + const client = useQueryClient(); + const keyHash = hashQueryKey(options.key); + const tagHash = JSON.stringify(options.tags); + const hash = useMemo( + () => client.configure(options), + // queryFn is deliberately refreshed below without resubscribing to the cache entry. + // eslint-disable-next-line react-hooks/exhaustive-deps + [client, keyHash, tagHash], + ); + client.configure(options); + const subscribe = useCallback((listener: () => void) => client.subscribe(hash, listener), [client, hash]); + const getSnapshot = useCallback(() => client.getSnapshot(hash), [client, hash]); + const snapshot = useSyncExternalStore(subscribe, getSnapshot, getSnapshot); + return { + ...snapshot, + refetch: () => client.refetch(hash), + }; +} + +export interface MutationOptions { + mutationFn(input: Input): Promise; + onSuccess?(value: Value, input: Input): void | Promise; +} + +export interface UseMutationResult { + mutate(input: Input): Promise; + isPending: boolean; + error: Error | undefined; + reset(): void; +} + +export function useMutation( + options: MutationOptions, +): UseMutationResult { + const [pendingCount, setPendingCount] = useState(0); + const [error, setError] = useState(); + const mutate = useCallback( + async (input: Input) => { + setPendingCount((current) => current + 1); + setError(undefined); + try { + const value = await options.mutationFn(input); + await options.onSuccess?.(value, input); + return value; + } catch (cause) { + const next = cause instanceof Error ? cause : new Error("Request failed"); + setError(next); + throw next; + } finally { + setPendingCount((current) => Math.max(0, current - 1)); + } + }, + [options], + ); + return { mutate, isPending: pendingCount > 0, error, reset: () => setError(undefined) }; +} diff --git a/demo/ai-todo-assistant/core/frontend/slots/LazySlot.tsx b/demo/ai-todo-assistant/core/frontend/slots/LazySlot.tsx new file mode 100644 index 0000000..d377cd1 --- /dev/null +++ b/demo/ai-todo-assistant/core/frontend/slots/LazySlot.tsx @@ -0,0 +1,24 @@ +import { useEffect, useState } from "react"; +import type { SlotName } from "../../shared/architecture-enums.js"; +import { ensureFrontendContributions } from "../discovery/discover-contributions.js"; +import { Slot } from "./Slot.js"; +import type { SlotModelMap } from "./slot-registry.js"; + +/** Composition-root slot. Feature code uses the synchronous Slot to avoid a loader cycle. */ +export function LazySlot({ + name, + model, +}: { + name: S; + model: SlotModelMap[S]; +}) { + const [ready, setReady] = useState(false); + useEffect(() => { + let active = true; + void ensureFrontendContributions().then(() => active && setReady(true)); + return () => { + active = false; + }; + }, []); + return ready ? : null; +} diff --git a/demo/ai-todo-assistant/core/frontend/slots/Slot.tsx b/demo/ai-todo-assistant/core/frontend/slots/Slot.tsx new file mode 100644 index 0000000..9978136 --- /dev/null +++ b/demo/ai-todo-assistant/core/frontend/slots/Slot.tsx @@ -0,0 +1,14 @@ +import { Stack } from "@mui/material"; +import { Fragment } from "react"; +import type { SlotName } from "../../shared/architecture-enums.js"; +import { getSlotContributions, type SlotModelMap } from "./slot-registry.js"; + +export function Slot({ name, model }: { name: S; model: SlotModelMap[S] }) { + return ( + + {getSlotContributions(name).map((contribution) => ( + {contribution.render(model)} + ))} + + ); +} diff --git a/demo/ai-todo-assistant/core/frontend/slots/slot-registry.ts b/demo/ai-todo-assistant/core/frontend/slots/slot-registry.ts new file mode 100644 index 0000000..fec7932 --- /dev/null +++ b/demo/ai-todo-assistant/core/frontend/slots/slot-registry.ts @@ -0,0 +1,57 @@ +import type { ReactNode } from "react"; +import { SlotName } from "../../shared/architecture-enums.js"; +import type { + EmptySlotModel, + ShellModel, + TaskListModel, + TaskRowActionsModel, +} from "../app-model.js"; + +export interface SlotModelMap { + [SlotName.AppHeader]: ShellModel; + [SlotName.AppNavigation]: ShellModel; + [SlotName.AppContent]: ShellModel; + [SlotName.AppFooter]: ShellModel; + [SlotName.TaskComposer]: EmptySlotModel; + [SlotName.TaskList]: TaskListModel; + [SlotName.TaskRowActions]: TaskRowActionsModel; + [SlotName.AssistantPanel]: EmptySlotModel; + [SlotName.SettingsPanel]: ShellModel; +} + +export interface SlotContribution { + slot: S; + name: string; + order?: number; + render(model: SlotModelMap[S]): ReactNode; +} +export type AnySlotContribution = { + [S in SlotName]: SlotContribution; +}[SlotName]; + +export class SlotRegistry { + readonly #contributions = new Map(); + register(contribution: AnySlotContribution): void { + const existing = this.#contributions.get(contribution.slot) ?? []; + if (existing.some((item) => item.name === contribution.name)) + throw new Error(`Duplicate slot contribution '${contribution.slot}/${contribution.name}'`); + this.#contributions.set( + contribution.slot, + [...existing, { ...contribution, order: contribution.order ?? 100 } as AnySlotContribution].sort( + (left, right) => + (left.order ?? 100) - (right.order ?? 100) || left.name.localeCompare(right.name), + ), + ); + } + get(slot: S): readonly SlotContribution[] { + return (this.#contributions.get(slot) ?? []) as SlotContribution[]; + } + clear(): void { + this.#contributions.clear(); + } +} + +export const slotRegistry = new SlotRegistry(); +export const registerSlotContribution = (contribution: AnySlotContribution) => + slotRegistry.register(contribution); +export const getSlotContributions = (slot: S) => slotRegistry.get(slot); diff --git a/demo/ai-todo-assistant/core/frontend/theme.ts b/demo/ai-todo-assistant/core/frontend/theme.ts new file mode 100644 index 0000000..d26df50 --- /dev/null +++ b/demo/ai-todo-assistant/core/frontend/theme.ts @@ -0,0 +1,57 @@ +import { createTheme } from "@mui/material"; + +export const visualTokens = { + ink: "#171923", + navigationMuted: "#aeb2bd", + purple: "#5546ff", + purpleTint: "#eeecff", + green: "#168260", + greenTint: "#e4f6ef", + orangeTint: "#fff0e6", + sportTint: "#e5f7f0", + shoppingTint: "#f9eafa", +} as const; + +export const theme = createTheme({ + palette: { + mode: "light", + primary: { main: visualTokens.purple, light: visualTokens.purpleTint, dark: "#3326ce" }, + secondary: { main: "#d9ff57", dark: "#a9d21b", contrastText: "#111827" }, + background: { default: "#f6f4ee", paper: "#ffffff" }, + text: { primary: visualTokens.ink, secondary: "#69707d" }, + divider: "rgba(23, 25, 35, 0.10)", + }, + shape: { borderRadius: 16 }, + typography: { + fontFamily: + 'Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif', + }, + components: { + MuiCssBaseline: { + styleOverrides: { + body: { WebkitFontSmoothing: "antialiased" }, + "::selection": { background: "#d9ff57", color: visualTokens.ink }, + }, + }, + MuiButton: { + styleOverrides: { + root: { + borderRadius: 12, + textTransform: "none", + fontWeight: 750, + boxShadow: "none", + paddingInline: 14, + }, + sizeSmall: { minHeight: 30, paddingInline: 10, fontSize: "0.78rem" }, + }, + }, + MuiPaper: { styleOverrides: { root: { backgroundImage: "none" } } }, + MuiChip: { + styleOverrides: { + root: { fontWeight: 700 }, + sizeSmall: { height: 26, fontSize: "0.75rem" }, + }, + }, + MuiTextField: { defaultProps: { variant: "outlined" } }, + }, +}); diff --git a/demo/ai-todo-assistant/core/frontend/vite-env.d.ts b/demo/ai-todo-assistant/core/frontend/vite-env.d.ts new file mode 100644 index 0000000..5dda26e --- /dev/null +++ b/demo/ai-todo-assistant/core/frontend/vite-env.d.ts @@ -0,0 +1,5 @@ +/// + +interface ImportMetaEnv { + readonly VITE_EVENT_STREAM_URL?: string; +} diff --git a/demo/ai-todo-assistant/core/shared/aifa.ts b/demo/ai-todo-assistant/core/shared/aifa.ts new file mode 100644 index 0000000..03a46bc --- /dev/null +++ b/demo/ai-todo-assistant/core/shared/aifa.ts @@ -0,0 +1,60 @@ +import type { + CapabilityName, + DomainEventType, + ErrorCode, +} from "./architecture-enums.js"; +import type { FeatureName } from "./generated/feature-names.js"; + +export interface Actor { + tenantId: string; + userId: string; + scopes: readonly string[]; +} + +export interface RequestMetadata { + correlationId: string; + causationId: string; + idempotencyKey?: string; +} + +export interface FeatureFailure { + ok: false; + error: { code: ErrorCode; message: string; details: Record }; +} + +export interface FeatureSuccess { + ok: true; + value: Value; +} +export type FeatureResult = FeatureSuccess | FeatureFailure; + +export interface FeatureContext { + input: Input; + actor: Actor; + metadata: RequestMetadata; + capabilities: Capabilities; + ok(value: Value): FeatureSuccess; + fail(code: ErrorCode, message: string, details?: Record): FeatureFailure; +} + +export interface AifaFeature { + name: FeatureName; + capabilities: readonly CapabilityName[]; + execute(context: FeatureContext): Promise>; +} + +export interface PlatformCapabilities { + IdCreate(): Promise; + ClockNow(): Promise; + DomainEventEmit(input: { + eventType: DomainEventType; + data: Record; + }): Promise; +} + +export const ok = (value: Value): FeatureSuccess => ({ ok: true, value }); +export const fail = ( + code: ErrorCode, + message: string, + details: Record = {}, +): FeatureFailure => ({ ok: false, error: { code, message, details } }); diff --git a/demo/ai-todo-assistant/core/shared/architecture-enums.ts b/demo/ai-todo-assistant/core/shared/architecture-enums.ts index 90ebf4a..d88b7ce 100644 --- a/demo/ai-todo-assistant/core/shared/architecture-enums.ts +++ b/demo/ai-todo-assistant/core/shared/architecture-enums.ts @@ -1,11 +1,3 @@ -export enum FeatureName { - CreateTask = "create-task", - ListTasks = "list-tasks", - ChangeTaskStatus = "change-task-status", - DeleteTask = "delete-task", - GenerateTaskPlan = "generate-task-plan", -} - export enum CapabilityName { TaskCreate = "TaskCreate", TaskList = "TaskList", @@ -14,16 +6,44 @@ export enum CapabilityName { TaskDelete = "TaskDelete", IdCreate = "IdCreate", ClockNow = "ClockNow", - AuditRecord = "AuditRecord", + DomainEventEmit = "DomainEventEmit", AssistantGenerateTaskPlan = "AssistantGenerateTaskPlan", + TaskPlanCreate = "TaskPlanCreate", +} + +export enum DomainEventType { + TaskCreatedV1 = "TaskCreatedV1", + TaskStatusChangedV1 = "TaskStatusChangedV1", + TaskDeletedV1 = "TaskDeletedV1", + TaskPlanGeneratedV1 = "TaskPlanGeneratedV1", +} + +export enum OutboxStatus { + Pending = "Pending", + Processing = "Processing", + Delivered = "Delivered", + Failed = "Failed", + DeadLetter = "DeadLetter", } export enum SlotName { + AppHeader = "AppHeader", + AppNavigation = "AppNavigation", + AppContent = "AppContent", + AppFooter = "AppFooter", TaskComposer = "TaskComposer", TaskList = "TaskList", - TaskSummary = "TaskSummary", TaskRowActions = "TaskRowActions", AssistantPanel = "AssistantPanel", + SettingsPanel = "SettingsPanel", +} + +export enum AppSurface { + Landing = "landing", + Dashboard = "dashboard", + Tasks = "tasks", + Planner = "planner", + Settings = "settings", } export enum PermissionScope { @@ -31,6 +51,7 @@ export enum PermissionScope { TaskWrite = "TaskWrite", TaskDelete = "TaskDelete", AssistantPlanGenerate = "AssistantPlanGenerate", + SettingsRead = "SettingsRead", } export enum HttpMethod { @@ -39,3 +60,23 @@ export enum HttpMethod { Patch = "PATCH", Delete = "DELETE", } + +export enum ErrorCode { + CapabilityNotAllowed = "CapabilityNotAllowed", + CapabilityMissing = "CapabilityMissing", + ConfirmationRequired = "ConfirmationRequired", + Forbidden = "Forbidden", + IdempotencyKeyReused = "IdempotencyKeyReused", + InvalidInput = "InvalidInput", + InvalidOutput = "InvalidOutput", + InvalidJson = "InvalidJson", + InvalidStatusTransition = "InvalidStatusTransition", + AssistantResponseInvalid = "AssistantResponseInvalid", + ProviderUnavailable = "ProviderUnavailable", + InternalError = "InternalError", + NotAuthorized = "NotAuthorized", + NotFound = "NotFound", + NotFoundOrVersionConflict = "NotFoundOrVersionConflict", + RequestInProgress = "RequestInProgress", + VersionConflict = "VersionConflict", +} diff --git a/demo/ai-todo-assistant/core/shared/feature-manifest.ts b/demo/ai-todo-assistant/core/shared/feature-manifest.ts new file mode 100644 index 0000000..8517b7f --- /dev/null +++ b/demo/ai-todo-assistant/core/shared/feature-manifest.ts @@ -0,0 +1,52 @@ +import type { AifaFeature } from "./aifa.js"; +import type { + DomainEventType, + HttpMethod, + PermissionScope, + SlotName, +} from "./architecture-enums.js"; +import type { FeatureName } from "./generated/feature-names.js"; + +export interface ContractValidationError { + path: string; + message: string; +} + +export interface ExecutableContract { + inputSchema: Record; + outputSchema: Record; + validateInput(value: unknown): ContractValidationError[]; + validateOutput(value: unknown): ContractValidationError[]; + validateEvent(value: unknown): ContractValidationError[]; +} + +export interface BackendRegistration { + method: HttpMethod; + route: string; + feature: AifaFeature>; + requiredScopes: readonly PermissionScope[]; + mapOutput?(value: unknown): unknown; + contract?: ExecutableContract; +} + +export interface FrontendContribution { + slot: SlotName; + name: string; + order?: number; +} + +export interface FrontendEventConsumerDeclaration { + name: string; + eventType: DomainEventType; + contract: string; +} + +export interface FeatureManifest { + name: FeatureName; + backend: BackendRegistration; + frontend: { + contributions: FrontendContribution[]; + eventConsumers?: FrontendEventConsumerDeclaration[]; + }; + mcp?: { toolName: string; description?: string; requiresConfirmation?: boolean }; +} diff --git a/demo/ai-todo-assistant/diagrams/aifa-backend-extension.png b/demo/ai-todo-assistant/diagrams/aifa-backend-extension.png new file mode 100644 index 0000000000000000000000000000000000000000..ce209b950238105fe7fd949b2965e4816991a5d4 GIT binary patch literal 98831 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 + AIFA backend extension flow + A new feature folder is discovered and validated, invoked by HTTP or MCP through the AIFA runtime, and connected to tenant-bound infrastructure capabilities. + + + + + + + + + BACKEND EXTENSION FLOW + Add behavior locally. Let the platform compose it. + The feature declares intent and capabilities; generic infrastructure handles exposure, security, and execution. + + + NEW FEATURE SLICE + archive-task/ + feature.definition.json + contracts/*.schema.json + backend/feature.ts + manifest.ts + One folder contains the change surface, + contracts, behavior, and registration. + + + + + PLATFORM COMPOSITION + 1 + DiscoverFind local manifests + + 2 + ValidateSchemas · DAG · routes + + 3 + RegisterHTTP · MCP · scopes + + + + + GOVERNED INVOCATION + + HTTP or MCP adapterSame input contract + + + AIFA runtimeActor · metadata · exact grants + + + Feature.execute(context) + + + + + TENANT-BOUND CAPABILITIES + DataMongoDB + AIProviders + AuditEnvelope + EventsOutbox + + Identity · clock · transaction + Runtime-owned cross-cutting services + + + Why extension stays easy + No infrastructure imports · no hand-edited route list · no duplicated HTTP/MCP logic · small test fixtures · explicit security and event contracts + diff --git a/demo/ai-todo-assistant/diagrams/aifa-frontend-ui-slots.png b/demo/ai-todo-assistant/diagrams/aifa-frontend-ui-slots.png new file mode 100644 index 0000000000000000000000000000000000000000..4eff916d1485d5126de2b3f7f11dbc08767c8057 GIT binary patch literal 99835 zcmb5W2V7Ijvp62bih>t0paSBhn+PZvdJ!ccy(RRb(gOh@5R__1f&}T(CG^mQkkH{O zBE9#}L|Os_1VV4Wc+2f$rGoTe_>`meVX|K>$$VPa9+50k@Erv$7LQt-pgG4 z+#DQNuV3XC621nyc8OQ?hN#F5L6K`BKam_cdGaLlDQ0$NW_FRw9G6A@e}}{00Bon2 zUY@wkbc7FZlfB=jxaF>di2z3M)Un&07saZjxff4;`niv zV<%6X1ROcabnH0W`4jBF-aL6h#lSFvPr~SdN8~A>r026@{#&TDPaM*B9uhbOAgW&8 z&tF90O1dYd?%LN2%D~_+qkmjfGqx@L+|w(Z4z*ihP`JdP{WJVUoss@dfzfn>?PvXw zV@Ho3IeP2_(@&5iY)6^S|9Vr!;28Ub2Of9Qx_=z!lXw=f0yHfCbVvrAWdI#zV`2j+ z1Ge8D%{GRC7=82^|6dsYIl5jC(gsh(BV3qsiSCQhx+egfP0v-7|PynNce$MZi z{~7%o`4d+G4~BjRJ;sRtGxFR2v-i&$;LiwjocZY=w0@TUn4CXy|Gnjp+TUXk;PyXC z|H%H1@^8Yw(-7+|Gg=!;x+9=Ac2-pp;QaJY%8YLRb`8H{`vcEE5dWEd2spv>^mpPH z{?qvLQvXNzbN+Jvo@>T@`~wR3&xU{017QD(?(ZA&6!zgS@ShXJ$j32xeNA5Y62|4N z_)CImI{@?tr!iV!e?k7Y^!V-nR`FNyC)XVb23++1=M?@c@SluvME~FK9fN=V+3**B zMtc1AzvBCw)kKbK{ssOYB8+Z34l5>s$6kEV>Afuc-4Al{1K{fab^ngO`gbx%^hJKo z%2U8u89;scpM*{U08Etq{NYybgjx|%p34#|x)d&cJbw;~jT>L#cBA@Z=vGoMjtg1j zw4?419N0=$0Sm>H7Q&4Q#N3oiUmy9*bX7mmm7lFnQWg&v6n=xuL zGE=8!CP%fpN}82S!e>0HpLh}mcekAGFYZFFG!62aMAKQvZBXaTweBgkHsrj z{gNz={!nJcrU^KExWbx>ud$h|Wq)dftVeaZ&Q}a;Ob^YtvukvL6xdC;MTcAo+YkhwPN9DbZzK^?__3jPCtS(sM2`Sgt?*vwRc8NzGWqDaMLkv zMpr>*jWF?ZE}2gB_uS>;=pqRW4=gUkGa2+bOKpNe=L1CO?k<@HM9cgO z5$YO17;KZ-v>(n^==)$kezNn-iry1pwbjurYn!OXaCe`rq*FZXu8X93u>`|zEe&qt z@d}MAlNG(0?O~Rw*vvI)zkI)Qy!VQR$OVxYp_-M?EffSdR=p++6>z0POm(*IXh#^S zHNaA9TSCK;3+=w2^vk8NB7#^a64KIF;(k8X-qyN$bQ}raoYZgeg1co@c|CJVEB}p* z(MX~DyH78w&FGE4K_!XKkZHb=kM>h96=?I!A&@sl;(hK_UJuRf6(GArmYuG0^AJn& z5z6OY!^N2*q@E2VXW=jimrx%z7zi(S+T;bCb0W##V_CX2eWb->B515Nc)G(VJ`{W4{_P0F^vj%&P4|LjCw)ly4);rEe zYH?$+_EA9%Y(N8pn_=LI1s=>c;TFgEhgNmyl3h!l>f!8x0f6MQ$SJ0;r_qoWF z9kUW8W|{2as?hAPE_--{c2h>HWXi^j%$mCJ{i&{**0$Y-xot~dAK3txXH2N!GZ6e- zkC$;?UL@FMekRNu+Y=gQekr95&*!ru$1%3JpM`0Bg6w=aUFvo@PsQUyRA6Hfk9?bN zPTos;)Q%M;-h(o3>CT@zy)xPHsMC#uHMl!DcFb#7u_Cu91N(&HBwgBxMsbyDR_Hb4 zPMhGyl1lqW+!NJHBb0C>&SAi7?-$)uDqv99;`IAv>**su;cX+=-vnHz7BVB!&zSe zRB66B5m&)k{8%_5x4SGla@XNNd#W)`%&=`!xhjS!ul3`Pp$%!bc<&6mI4;*NL5L;} zM?pogx>pzk-CC4Y=}bLS59!VBpXwW*2&Z@Ph;Xm3F#T>01{=XL9o%`;YUNYp?DOMC6ES(Q!}qr)=&njX61B)!BR4-d$rt8&=U&> zdZ{Y?B1kE%U_CA8*AvWU!v+F8W|Eo4GD)E$saE(b(tN902XcKKod0M{gfAy28xygb zj|@M*0}6dL9rD(%Z9L^k*&L6sMb(!Sl=)+MJ&j1$!cMXiU*<5gbuqduzq=!b6`F-g zdJ_%erNIG88S^@+%K#r* z^02%r*QNV~q{R&RdoAS&yD#_c&zoA7_q~0%_IX}gh_ichdWLfcZ0YAeFAoD6SQz7_ z1QjiSk?Juz+AwPc&TEa>8}6xUKO#qHt=ZcOzDfB}y{0FYdj)aN8_Qy%rSJ~Dh7vMK zLu7moCLe0;SQ~=2e&JLEB#b8=0`LW;OZeosaA*X3lxLEG45(1%$L4fu;pT>a*>3qk z-AI6+(}%^~TVr8V*20oTEdsK{*v(3i$5Im953d#Y>@&yx*byUkp-W0_&ZXF-zdm4i z%bnFPk=;UI;mV8wobw{IBj}mCo@6Jc^GeMOfwZ`_atO%!$Xe6OPjYcw04;LR;<7Ng zKN|VL!}1ZfEi(aUJ-%U*cD42|x;Bsq_h&16OmjD|^cXOAbW~r420MGKWIg!Poy{~7)i9T*KC{?quo0QQHlXnd=WuJ`25x`h3F zP1E^|rz|%BKT-W_IDR|re-V#P+H~YB}!5KIlV0 z2!S5wm0p8vCr`R22B<59d3ib;&+D-0YQD*B;QUB|RjCj-MV}6jSQ!rvn1Mwu(iWU^ zXFV(6Pe8;Q`B(%^U)HACA1XF!K&Wa?U3%>V|M=CcH^$ zgbc%asVml}LQ|jg&CBT#jlA+i?G;VThb!lusPi!oefi-P^>jR6A<(ZJSEO{3`S{d8 zwcCp9f$|kkU29LXLqO`j^!`Wk8T5;9mITRcBwv8m&xEKK8M!8`n@SKOuZ$$ z1xMf>AwX-lK5F%sMb8B^q=tWeR3kfo2#DLzs?ux;EwJkr%bAW}Moe9Tdg6KV5bh(w zrG_K(m&)SK7=F^=k^hjLnbT9KjjwnV(uh@NdIc0`&77)LOxArlZ$H{eI|OV<%R`V^ ztvz8t7qK1_^AC8t!lGE=^py8dR17|W5CfZOJgG925-n;>DrgIM3x(E2>dxB;oe<1LM>W6frP84Cl>jxpAwVg}rK znUo};$0v^!IT)7_L0?BO`o8p&Wg!9x&No9RQsV zl)jRg=)QXh2x2(|q=%&uhc_&cPD^n4gwQb7*_gyK`N#X`g(TkOBv~e{w5@js_L(C~ zr4vu|x|=ywsfBs1@a3iX@?||r@-r%ajC*7)taL2dLQ;t{?ryjkZM$>Tn{4s5Y4J+u zxEaC6dD$d18m!xx&l>Ws7}emJvbQl~AS29fMOi%rAWL1xhYd$TJ`hf3o_0wu*^$P{ zy`OY@Q&@iC+!%}1(@^Hcnjx!Ff(Qf?dW2i8mx_gJWuG${f|c{X?gr0sxzewWQ2qR< z!?SvA+~eKZwR!6&bu3<$3ceOHdt=^8&o^572|!NsfDgH&27M%1e$Ox4?2j8P8BBm% z=?KEVc1#zG5{(ARYe{wM7O&&WOnM-;s5SnGrrE4$D}`Ix>>)4wefP?nh3iv|R^$S- zS|(N;x1|okmP-f5BXY#0E5rIuUoX)U3zlb|Rg`>27YpI3c4R z)y@Wq;F@SMMqV9SrQvGtUs{%cf0L>`6hWC$ML|G{?598RwCm4(7^Kw#~|Tx9|g3>;;;0yQVv9wQm5-d2TaRM-8rmjKD*eg z8dcm3Wb@-Bl+HaJn;&T`TMvZRtAOm$Jai1pGp&kON3rcrH+6+JF~HQOz@c9&-QmYU=Ft(4wQB1e$B?urr< z$SQSGco6}%o&bF=C6K_~aV;r}`%RYm!v!frWc=#{`~sXHe^(DwwE#jl(RP7w15V8! zUp=r_@Tiuwc9kIerRw~RR9s+1Z)}@a;QI~yaH)jZHBq>vNoM6qby5HPuK8fG6d4%w z0Q&^xne>fGVSC)CvaE;N5qGIR{5OCE<##FJujqf3V*aBv`S2I$A6hWr-x?FmEN%6a zb-pNDZMD8l4oPdwQ{&tJ0sb$U|4YI2N0$o>_&+pC|LJ(d58vl^IuOcZoIa$rD#D@i zXIY{4l3AFCVuhuUdG0&W=L^(XTX-i%jVOyJ8l3q9+fTJ5)2N4rkdG^BrBl=H9AIN> z;_`Pj*}P6x?JEqSikU%Wqscx(lH1+Wv(+4LVC$=VhuCyr?(yZ^)$%RDKQPL}@oy~t z7|+!|lAkdU@ZpHZyHNfhPw(jWEud!+~5jD-@VQ>Z6 zLfe=^s7q7==>AXIjBxbB)t|``>RW$o{pC4vPodkZXU5Ef9*8%k=jkm9ZeKWawep`M zG5-nvpYW$j^5?w$5&qC5GE{cw48Cr2vK4p077=zDTvK0HuntcU+DE4Ugkv+#vJ4r| zSiNvM-dyLpCcxLuJ;H;Fdu9n?mtXy&FQQh#{o?FgY{sX*I{Za|p@aQ9`(JZ^2(VWf zm499w0KgDR<(~ctb-cxg-!ZJil1~=s-+KRC?IEB_SbgF^7`JV62>6lc&^|%)({e?A zZZP9PvM1$h*g{oJgl~O+6X?q8x_?1A0Ti^_dl9U|_2^QieWP~QgAWuMC z-M%t{J1MHdK-Z!tYuxo~|9GWR#_&Qlu%8VnC{SeQG;LgFEZM+L)Spt;n3&7j-Mn@P z`1rO@PU65*%4mP= zWxzlCOtP>$N^#;W>wb03iq&bCA@t2eLO{^Wz>xbzC?X_RNkDe8g1&n}Zrr0}hEZSH zlFMN>z95dq4ljPe+&E-9x zx(d8eqPMyhUlR4uw70f=tz*q>yyUv$OyAk56KN)g0ROrz<-JusU8`fmR4F&oA)xr9 z64qDf`uc@?>;9fv(mT;Ax(h8yvEflsbbNKEN0*`>0g#(u|< zmmMG}&n%*?Y11xA2UqWU#8S#9Hp1|E$S`ioX) z=uEB_5YYv zdLIIC%4{BQm{=LnV3l!If<{(P@oyRJ zag-Mi3oVW~<)AxzL_gz)&xT5-3SY81FP2$fHF(uu{~SRImCpJW%>gnO&B`SVSZAr4 zC@tuJ&BxT9N-9^Ii#u+;Zgj0|?D!(K+dNxKr!u9}OAY@?5#;1h_{ERC+qlS$U9sh4 z{Wr?kIWHHbF=Pf~8|Kwezm3_MBQS4FlD(t}hJ}w;RiV}jan#DOca-q^)NilX?mo;y zjZgv}=H4n1cKNu6U!ke~#(VC?lC19yN~4==VGg0oBKG7=etv1wThp z29AH$3^EB3$A_db^hl3wkih_Emi$<$YC+E1qkfyZvz8e!Ag-6ZH_#D!kVLkmMz_MV3G|Esd{Z9tE*6M?ky9X7Nm^+* zmZ=|WYlDz#ZtigpN_P8v$2|4F%97`T22I_kp2)VFLi7(xWqvfGXkTyIyN(DMjz$HZ zNtv404c|FFY?V+dk%vfoP4iT97miG8iT?4UCEel^kp0WzYA5S$|A*%)+3#0^Ml>}h ziTb7>ALqWY-P;_)c%aA~p81P&Jyk|f}c5Mx(-L(|ye)ubc42AbsW=NsC%-hf-qX44zi zi|dV*kDd!vsrDKq=r5*f_#|3gkWaYa=aBUfglFK8Ws__?h0opM)pd2eTg9pJ(YP`! zv$(g_vPG!RxD%X$!5S~1A1za6jD9c;c57{?XCDHtr>@gtIm4lcRZHz?oY<6oByW3RBF4P}8j${yK2a$+3lZ;h0aG=vKOPF$egT z)O;ue*ph3uW1i5|y{fr1C=>wOl#W}LWo_|-IE?7NL%*KXyAaLAdDnE_{PC|5@P3J# zY`mGRF=_~?Zm!?Skhk(y(soml#+YkN=)a&ha1IZ)E7m7~2xC zmT@-PdR{f5yD#V(*yUy9Pckbi?e|_=l73WN*jn1j6__gFbA)782BaHC7z^aU8?QTB2R&)5^r1*#>m5^_NU|j*k;#z$u9}wp z5d~NH)Jqx^J{i+_mWfxEsW^bwS2)yte!kG8OtTfp-UG37$?QKrB_}!b27}4##Mss- zy~!VT3n9TDrdR5_63=4YOj}>z%z(0v5}*pB@w5IQ*D)^Fy47A}0h*QAf4;c0jSQix z6?a++8G&Ep1BC}IOuAz0Y0%_OqJHlP!rr0Mx7Bwt$w;Kqz^fv!o)9N^S8wOWVshkj zuD6>AUnsfC>U$PemIFQx;bJ$Rc0o1BG&l0#&)%lir3WMq=>}hE6+JI%K5#qIxHGs@ zxL{xG%ck@x|LQ<2%{!&hzf|-}Z@nI*!p^0ic;3k^W2RB$QNY_Fmat0t7%8?0?+kM7 z^Xlu&42hI&vrXm#wqUSR4JM^xO6DiMcMM$vUfAWKy{lxAm}kVB9VHoVo(sl#H``5W z{M$IYiyUZNMm@)&EC;XU$ZB00AhmUvs;|;5=(pUVyjixEBXqr<;0GjkyG3J-4RkNd zSwde;q@1*@H-sx%7nH5ZU@08(O$9xOKqMwA>WJFINh!|ujOQtZl7L@gUDmh#{#(Fs zNivHo{B`T`T6`xIm2VuFkX3TaKeCEw9y*`xDIHhyI4Rs=hl_yBzPEeVOydk~_mX>_ zxhd||d|__Pn-D%oN5`c+aP%Tfby+1m@zbW2ZX?}*5XwTDav#RXW~z=gT5fF?>6$?! zyu}-Z!g?mUce+g+b1D`U<#U$wi|x#XYOH`ipC{am`O=~oXDqwy2_+p7Z?R{UH+3V# ziMr*j0vs;Bkp45gC!6LRwUO|dX1GZ)`$QCF1lvJZE|$m3*RXc+;-NFL-}pqV1-tT5%%_!t;lnDBtYbts1=Xs;bZ}K8m$Mw^7s98j|EjutR4}+LllD zUx;)b=$qt+vzbqCg5jJiCvM#5;ZJi5#qX$9wHBIm59b2q+BKy?`08|r#|Rj0I;6pm z&1v@AZe?7$SQuJ&yrH1f{1PXg$f-_(`tI3TI*z`-N>jcgf0`)O5IGEGAvtrscM&bD zR~&yX7A*WNbTsEKL;9%jvj2EXN+~{jh*n2ZF^{>hY_48m`PMf(V~}WI^qg3vtC)** za!`ue=Q5Le*yvcb*iMqW=(CiS>x!6B2PfuYD&SvlDJow$q2vt;I8C>v%(oJaTW5Nm zk==tvB@RYLQtGm7g=%>z)j)af&vlwtu=$B#*WxiL#0byXa|xP==&%kLDkf)E9BBQu z`rZ5h*|L=ySRFH72I{swcxAB>38u~G-8hLAZ35cYQ0;`#bVPthHm?G zKXa#c!pdAn;?P}m z2ZHtd(PICopC-Soqj-67{1aVT&Mc>Uy%7IQpfL4Z{GN$I5H4kQ@?^JI1A!=|;kN$H zu*%7KNup5)obe^hVJ3;K;(JQet~_ix(ZZ?3dT}Y8?BqOK1}4wAE_aP-v|{x`!{ZNL zPiz!9DL2LM<1XoCg^#!fkL4W#nx$$n1!zBM-70%1AwmUM9J^y5_DjS$E{DyOx=o_T<6i>Dvtm`m~FXlbNY zU_vh;j`{Ka$!4{DzxOq*{G5j3-~D^(&nX?O;YFoU<~-p1!3&yLbe zY>S>N#&f4~DqtO+xk>&9`X?=RAY>BCNva3mPUIK@8)n7ZXqLz}XQ_$7*_o5bg*J4Z zQ0m?8u~fTn#iJW;<1HYKWI@Q?i-oYkmncq55l|;cgqtr$5n|&ZvU8GB-`x=9wBfMe zL)LQjE(kC*>J`9xI;%;G!9Hh0e1&%e&Si9bBBKp9M zc2@do4uORRO~`KY21ZpxvYBK?h~1b3)Rz!C5#7_*RsFHr2!kFXy4t8&sJKOf6^k76 zs8jSSI}qE^{w0%QZ@vS87v%GCs#*f#XD)m@;G#Z~zTO{)sW7{Hdjrz<(O-)ZM zFXY6;%H0`8hF8~`w`XLg#ZK|?l?ZoA6gPe8%YE6GBvB}|Z9Q@B<{-Vq791!FkjL>)N!YX=bM`Ug&o}yC^=(~JSo9%4-Z04ZHzf+yjcQ;| z>2(HEQi}T;ROwi?LLh@h)rD&$qqlegX84)(8vXvyn^KPvm%!Gc4cu5LwDZeu-9I#7 zr!w~4cxxxx&UIgjx*SfKLXekqOZ#yQWsXN?0H^4RftM^ILFl{YaCQp3QCgrdOnEFU zi`JvQ=0jAT#TPAFqQY^=gEMhzqC^O7!8jp=-#?&Iz_V%4A81KG1VHb690Pe>3LGgQ z7SvQZiF|*q>gx4FgRB2xSk9ZU$>o+kxNcO}c;n~>m+e7Dm1TPQQnnv;f(w3LX7o*^ z%pt(Ska1Y?z$PyS2wRYPvg+&$<@Mlp1+Ykw0-|KawXv7#H%%M9#cSiHE?UziE%>{w zC#*=uDJ7ddS9*t+dO;P=v3N5>DaUSgp%qTUY;MQbeyePawUHY`1!FhHjgP67g#M-%F7gn@pq}j1CFD)_mh^re$$&28!YiQ>y^h0>2tO0f~R<+X$yj z7MBUvT3E(xgup$+o?A^j)p>8*2>)7Os)W_=vFMp|8qu6HsXZQx&B7`b_M1pwx4Jhc zmSXmK6_O}6M4usS-YU~`t{CHxrgWRQdFA`m`G3*Qt~30=DfeKAHL8FEj~PbeCrdf* zndV8T%TJ*$CXH^PUTLFXa2kB_OUpiU-7dp4{fTi_NL5#tD|kiZEXQS8=SLc=6{<@p ztBWIw5W*Ka%eQ+e57Hb);2}W%e)UYw6lJ}qzXnK(5zlxHFWk8|o#A>8@8wj@y^fBC zZ}FS(0{H^Y&I`Ep(O4uDN3y#mpl*-B3444~`+{rN_a*9R#PGebL%{n%=$VeYf82K}78 zN)sW-qp2jj$wPpqAKsKylx|T%v?O0{p116qowlWp=5FD5(w%d&$X1vF%qP!)>+5|9 zISbCy##FeDwx?{h0cY@qMg5`_B(GX~dg<6Tv0GevDnP621qs3=&Y42Q4C{7J3`xFD zZC48_X+9lL>nN>YaAg3TQ1>E&^M{gU@gl>7x5JjPscLA;5_3#|IuGN-OgDOwWEU!aN*#-LsFF*K9o?7PAL5 z2owqKR@P)mFBl$wdnI<*=GfrnECV@XG427d^&G>Qvt9YP(Jbq8bY!Z1r9Q!A@ZL~P z%AAqzrWjjT1isI+=pla$zINJZku^a|a3tts3w92Ik{{`uWhUT7FCaQZ^auY6#Bv$Y$c1V!Iz!!!kln3FeQ50_)e*kIvd%7h$-UHo zB$6$?8EynNB6HSOG`sB zBG&>5A#j=NQPMt#0FLdTZb&6hML(UCqO&BD>6f3h%a+rp_tgP+Al%!Y{fcM^BS{IW zzIAnvVDCYh7aHS=Ji%kLAN=R&O=Qvjl19S2x}FqutI{#Htmg0m|CJZ?2u&C2_k#&$f4o zWr)P3-Z3%nl3KHQJTG5GYYKBU`?rvns*9>hixfjVVjLr7jfm$FsL^<9BI}6{Yc<;B z;|1t#^aRZ8b=JzAJa9tW?8O}dM!(6F-Sx9_9mXc1XNSF0aDj zTtA1hvo*k~=hX;$X0e}X8&Seo%JFA+9tk2C$6{Ve`l zpDB6sNkxaeP1goa!&j!_F8}E4U6k)lAS#nXWbpK8e9Hmok@DZ_Kb{ea$06XpyYifL zko`qq!U)j4hZ@=P#Ro>r)O2h}zau`GPYJl`8rF&2kR8y{X#xAT%k%1QU&cqO_QAXB zcgURk>@hUp0ZZzRxqhoD9h*>ym6NV9Io0n61MW`4=fsk6~r5U z848Rc4K4TzrR36}k2el6uQR+n9iG)MGG&*%iy(qbY9*)0jMKDZuN zj0c7IW=<#*-gsg1?QS@a!lGN3E4sZ=uroueU>-~I3L))O2)3?J?G5ZaWw~BjPRihq zJ>oA9I8O?luZ<8cZP`;nGn9++GvVUyyH#KIbg$S;S+7WS(s)Xibz)*MS+d=d3(lUP z>`z9)-woEKd(RV7q_OacVMPG8{ENrFMKZ1Imm@`<2qB#{yBs|TzY7C`=c{{bnZvUD&2RA?Ty61HuWHfM3VlVcvucowELBF`IBLP$RL@SiZN$4TU7N?5f+cOXI`P6i3 z#=?gFgv^sk_Dh~EauzW*Z=Mk2VedEZw}I9j(-!{bql?XtV@VpSvPxODB=1zp3<%~R zK>Y8qR;L@oPf*>z`FHlfgEOn7hD}tQ%4yyKB{&}q4G0jndXwQ61R+~dz$mQDOp5yL z50p^kyi_38wcCB6r%!5$}Qt@y!zq^E$y@c)+Ag6h5*vMq|S zMF_DKn1CackHPeQ>(_l%VsXZc%Ij7nMV220dc$auKBJ)VO+hUSezROo=9!KRYsD9t z)}dm_+)!k|KqV#@9IHmi1Ml-(MkU`%5dg_j|8J#wPmK_5zbw5KD*AO<$IO65b>FHW z(*@o{11%P23!jDPG6XTQk6qBF&hCD+@*&^`k>1GgK0v##MF!dbx+-E}g~|?$5KR`c za&@uGEXbZO@YmHO72{|865l#|Zt^YLtC zK%VHe7OJ9s9Z(SrxRah`ZXSSG6k=mDxg&ilq`6#rBK^3cyc}OyKQtI_41?*w_GUVw zd}nbLesp5C0JU9CznuQU3W>w)?A{#@fdXqv1kQz@e&tDcUif6*`pWcgyjrem9Y(I$ zR(;bP5D0WFhdf)N8O)zj;aTCC^Ck%hXS?}{?U(Hr54+i^e)?hB!UHi2TT5BZbep@o zvSuX#+-9zW5X7Jg*MtNFmAM?z14fO6J!Tw^I&Hfj>{!Fx1GvY)#i(+&Zf+`Sa%Lx3|WbPub%)lpB!43A{>(p?l;uo3ItVYwLCE6kfY2oUvlZPv3B zM`N66D>cbr;7+o$SgJ`Q=<^(;HQUG}5pEPUGO{hAWF45mTeO(Hh!2u!ccHvaJp{lI z`vVn0rEjPcnq!z6B&4#z=6+g2O&@D~R6Ub-bLI|^J4_e!CLd<$@%^KzdW?G85*Yg(4V6?dJL;sWy<%Ae@GsnF7N5J0eyKQIyAJ845H+AbazU(*ra5Ix!Y{YvCUWbImy;_UoRZlWYu04bla z4t*^pYw)Ij@iU(%ttHi&e)CRi5Y+D`chF|(&Au9w#8i+wciwVq-?qlMV=PCurWr0-X(1vGjO8euZXUYg7PF_q-HHqh_fsKqnRwU0XoBZ!OVU<} zMIrO~4gn{EaHGQC&`V+YCM@wodG-m*mMuGYsp}g0eK2)NLyE3qdwmeo>{7I=)%D?x zo?3a)dKZ@hxMZcdh6p}8;;n6*NTahSr8il@qg5enb+G%&DBP8HVSP*p$#Z;oC4KH0 z!ai1JwAyP%K-ZGZB%J%1BTn%{scW`Y)sqq;3VJW5448v6!9y^M_BA_(0~Th1>yybPwVuSIYCRABk-1 zPCL1lAq%pd*5Drqh%(dUU78JUpr?0SPBNS1ZcZGCtG#3kr45ql$L9qV6Imb|*QJvU>FUpnr0OqOhelY3Zb@neo=i!iJRpz1EQEUnWwe zK}lKZ)6Rn+so2i7YMY@pe}86%eNx?KE|KAQy%jtel`3zS^V=QF^qaP*Y5A zC1}p5LSu~&z-PLZCFgB2q&lsn64N#DTs&a>!uiLU_e^uJD1%Xt6qiPsGe4%3@4D}r zl=4n%A9S-sQDeMwpLY__ZaCryD-f z2NE@7?F-3cE<(!%nXU^uZ+CV_sl8Z*wWUq~alNKzU?HmoZeFNs*8?}GO!1-8E$1Y) z;&_40+n)m*Ni~;fiouQxODpu^(wn{Ax2YLO?bRJq?Md!Pl=Z#$)}Y|#XJEyTCUc8l zhlh)_;i~Dmc9DE2)3xC2@BN;E!ow5yxcH@_aPnU#xvobSlO~xG82-z zCXy_W3kL8dbWEh)W=tb+=lZB)75y18?lQ3*Y6(^d-FMWA*=ygWDwgsZ(Om@3>NP+I z9D0Rh)7g7{_Y^O<@2Ep|t_G14H@CTzr|#?tZt_3axo-u!TIxzGxyDUKMQVkQXDkxynlIQ$A;|3S3ue}(6CQ|Ff_@RApAqQuf}EygTziv zHB>g=hM$^5w3<}KdMv}A6F_Q=Hdw`}uZs4O}hZT#@zVgd(%*g z@hZs6Ez8x(-D~tm*`&dqL8}eS6Z`B40wnrf#$%MpHEM+kk-eCe&{M)kcO;reU0b(T z-$3|(dN4d5DK+Ryy;?`w`3N;^>}$4;l(=X<$X9i$-0krrmnR2n)PorFLP{qhqWT~| z$3|on7u6fFe=g{^5v>D*b9-X9CNdk>BHS2m#|`_XUnl7be#ZT!ZBQMZgp(k@n4-q% z%+w6*Z%C;=B(bu#-X*{ujHEs5bQ$Vnyg-kIS!uriHoX*TPSIQ^`AO>d zrBzsb$WHEbv5mLMy;WgeQ4g0*>)a@5RBA`^1SAyRn@PdKLzYA3ENd6DbR^&2JAUW) z&k+1=F#0?9^WzMR4-zoK(Wk#Be?>RGF|mI5s{lAMA}2x}+&)rrlc8UVsxx(pUtQkW zSdfd0O&+O}42fZKrpmhn#mT$al=5ihtY?HtbB!$}vk_BBv2${$*3vJo?Y;Ag8Kq-o z@&{=qK;s4DWZx^;-Xt-}w~oxVw|REm10$4M+QTbN>mv7!ilCJf|Bt=*4r^-p{)O?_ zu^bVQt{$p{ra-6yD!m&>2rZyO1VWSE!3IhTQbI3E2_!K=N(cl273l&}LMNc~KtQ^n zaBhH8f4}d2?|Yx;-uJI}o+o>+nOU=DviI!0XRVp_nVq|DyBau@{B*Nnvk!D^t5I={ z-c=lz4!=7*+n&E|5aLrt(ub}Xd-P-AXzC6er&S^5W9OaL=oRy-L5{~oiib4>Ie)iE zr%qjex|PG5s|zhL-2mg{zQX%86KZW|~N&#uZFDqKwp z)3wU#8zC9W~DLB z5Jg*G?(k%Cy8mLI|857Dy_xQ>RdFA_Z33jKk3)flcSzcYQ_x_-`A zGHKhthK{eWGCV3KzLDl{(5O?YFB2>o%6ev#KL`amZF{SR@wM}kB808U_l21+hBcCu zYpAQ+$PS+$B~MZxBOmoh>5PpEZ>m4o#?h!ax>O=Y01?*C0Y= z2xL9QvT($0yK%-iAKBqueZA_=%=p=?9;ja3s65Sm=ghhTWq4^Bzp)ySw;kIk8?>6> zuBoChCU;@)ANj&wFF?%46Ey8#%#_4^*jAm=+`KdS@Cj{BH7VIU+)O2-Qpu)~>?;ub ztuDSn$!yMMt6RLQ$#P&8f4kaR_G%S`{MOiWyQAs!7Zzm}X~xLsY-iUcBejuLI3Il1 zIK(dfS+6^Jai&iTExS1SWhT4F{OwxodWyj{`WPh$&wdr`bFl=cjJ}pibutQ+$+|8x zw5E&oPUwzcswOWY!xZ>MHTbEaN^dM@~UbF9KNgJr}S$ z@yz>GckGXRCA3ic0-AJ-e*5$LtYPTKDH};&1$fAJjn;I|U#6~in|AVkuCWkz(=KUg z$3`ca&YAC`qy;6k7oNZXKo;v1aejj0giqUO;`F_jhJ|Fv%^>8$AiHzgZImwm4-Mbf zTR9q(9npg11jo4w!{4$#dD)@dl;V-#@ddX;1sjEyRh=uik(ru~AXJCv6a)&uhH#YC z>BeC;LI+)Tb(hE}aq#fi>+<=}AKhNNrEoQRpj5q>0)vPu9aE?l>c+2(RkMkMr(n@r zQhnPoR`}nU`gm%BAzp>vl{3`BUzl8zs{MS3YmaP+$)Xn?6R-qCc(hr?Ts#BHV1(7w ze7&xF=hKrR!Ly#*g>(j}`D-KrOskm`Er9 zdAC_8IUzQ2+-ZKFo^@2~hgP`)7t!=UjE|v@I#Ffu3-Y6ekmU9gck1ch{>-dcF z(od%_EBQitz}jRCI;eb!Co?ziO{J%l(1%rk0u+fs16Jg}`X2Mo5WCNtZYUe%`6Rn%CN1_@)gx$=!a&KGJR3DP{q<}i%%?XDL;hT${X)@1Vjd7Ul0cYzFzakKQ~nAA?wJ!A0gf8er}CCPiF=^ zZQ^~guEb%n*j<06Qc4y14%K;-h}X*VdfItpQQZn~{By={Yl-Rgr5n|TMxWLPuQ&}F z_o==aU1GsJg((*_9WkOPR1CYG{$W8((WjfrdzyVJ6$~W04Y$1RU{yfyJDhC(z?4o8 zVe_8*`EZbPA*^NWwXDIG-%gNEmVTQNeJrj0|M?0(IMh$(olE(VF(4+9b9)Eg2QVuP z=$BUtIY#3Ays%x>5oD-e03+_&x(26we7Ri%JH60;1gYa$;LY6`9!;pvcG5cy z7nt)?-su3UASpQgx15d2hMb4`1ML_$CEwf;Uw3=Qih>PnTqU>@10~(Ay{cTVnt3yM zgGotEQ$=D->AKh4TpC{p8tNRv)&bV=b55OHhXnZpq&zy9E!JDvy z1NhFSlUuxb1<3Rzt029KftPSXD?wW|T^=_kojV1rJDFtUuc6U-{mZr1Lub6H&$eM? zZ|^db0vWdp0b_4N#KvBk|GaD6boT-Fd77rC<_FJdpVp-UzA91PavpiJ?aV+?pt|Kp z+rV5var{cPPcr0Fp4kRlw>S`Kq-jim|Y;lU)%_aa~JM zP27XwStFVWmVgDtkv0%SFzq zIBHZ;RL)tE--fbL1dg*w78Mmr_7xC~#>Rmx;q{BXhJd*7G;I;tg$0|Q3?^rqb67Vr z+;Mmv+1^`$XYJMja8Cm^(I9rU030|($eq;{CJ{yvho@&zBM9n>I1q^D|U^LXyg@xCCGfw(AFBgOx;!^*Am(U zE}d?UEh@+mV{6VVjchSRvMZp2P`7*DYg53sdir*lA!0Sqg7^E^7rj7wX9I|1lUq$U zM@PT5XIO-2?wHW2t_(@kub~26ZWAVSEO$$wryH<&-l)Q0ntZP2*>mB3GKPY}-1iCQ0jR zeXsCx*aV(65L{1m){?4x7N6=VAjVXzi=`+ z$SEU=c`gMa=kDNqfTpJUiCp%vt$-5xWKpGY`&IPmL+cDpTJx7~BB?zS1pujtbag~E z1$7Y&3hTv|uUXw)%OJeYO~`=1rjuGXCh)z@2{k!B&r>xwvBmGUK4H$-;ITynTspSY zNd%KNFPP9g<&`~Ujl~uNmE_g=#EiT02(gUkR0%N|>!)v8^?75aTeEx7aY5?r$3?}| zE3_;b?;3esS+iEHp-|joq;~7{I(}ol^?9ro`8Tz)# zTZP1!1)6cfGBgge9e8b?kMMk0RZmLEP+0->v>hKhiirl3StIUr&$-^-(O-7}1;=$- zjyd;QuDqnEsd90>GGGwNMZ zp9}Kqj(X&nww~Mpkt<<6OIsz{L6!`10l^AC!CQ`pMxB&G;37IC!=kwRC^|k587taX zt%rUF$fFDszhde+zfjTm9%H?*(Q6yeLW~(>);Z}=VlCCVYe#E>TjgiReWsCZ;o-2w zFk%iY#CHAJ?4@HfA>{RvCbJYjKOT95tgDR2Dns+=uf1Vr>lS^kA?_hK32>vVJa2~q z(_9VPT2t`sw$Lkk(8%jW&pNEZI-(KRZWqRU z_jv1#2(q6(-!o{7xighW#)8f)o3S*d~u?c4}x_9UXE$j4Jjha@k zpOUs^Z8>9tB!QUa;6)DP&r9j`^-K?{F-4&cW7lfB^GAh*@T$1&Qf7&-i`|7G_>P_i zsD6MzP&K&OTfOH<4d39rYe#tq{HVsx-JCJ2tl{!J*v@eWC9Bcq$u}X1`r(c8Z7;pw zcY}8XtX)^hW9uO*Y({pM$CgTGB&`?b+-0@U*9JcoYVh$hbPHTThyx|ee7(qokB{+z zIosh?FeuAp8VVCYt0vZw)#;Mkc{J+Fgu;1c`gF~uUbky`W1s5RpcQq^5}56oB8E@m zXH~wAA3pPTNWA3YqU^UbTRui9+t$0Y73thk7*ztT?Dfz#^@o`yhVlMV%#hNq)3K}; z%06|!YuMJ)Rr$OEHW$SsoHYJ?4P2ixmNeBQ_Hq1Os%r>4F-%vNcE$e5eepK^+iPGw zY!wTwGw>QKbM3lyP*Ukw{^QPj`ab<|lM;J0ZCyI#?knLo#-#CL3fDxvSWJ$}qZf<# z`P}SyF{SIZU`V&A93cWz)EWh?)2m{ESQbqhoi!-twTt?u41{(ghLULI)r#g@%kN8j zf%wh_7U%a8q1L7BkVtsj^{L9h#AY^0va< zt?aoM2m~4=2|KlOq$uqJ!K0&5(Ig%x?b#E^(Feo)q18V;$Ti8^*g z%M;>sP0hT&HEt#=Oq5-hGOwU)U;)fp-HM;zAJx3x+8Q3aA`K-DI>)OV2U@1u2!0b2 zt~ch>=iQ#QaPbkFzfh`^-7anyPtd%F##^FT`KoTI4p}<7dYc_3wXxpo4D!34VnGLijn!5XJ>6xm#gX3FDKB1ZtWm#*(aFw{C+|v1Hljnik-sxZe8}^rDG8-t(rkO? zj6i(pXkDy*yF;>xYlK~N?;G3)rOKSGc88DPzphg_%(E?qT?Dr0{IxltwoBKLjDRoZd zo~EE&+5Qit9^Gevi4BJ~jRNizqbn)z zVP}+DSZyfLPu7L`o`NA7AATf-m;Lw+I5f=eO^NSTzusnitjIpzX0j3D%|C~LvjWVP z%adst5yu`4WpHU6bM=%kfNB9_vl*^3o?`~~kpVR??j2OEMBUrb4luK7wQ z*N&AjW^?%(`Kvi?%(WN`k$!rbzZ%Xi8cZJsOCiYtLRG*OG4dz3 zRyje4K?!$Mr8RaYt`d)P{aVpumHT*9(&=7ye1{$p(PAt%vZJV#OPs}~dir@@Jj!|p z?W2K6;p9;l5+@pBlSR2AJ{HAkUo$lUz&R48*sq#o+!e%PBb_8~_ukgkqV@G^o<@3> z6n~!1_wOZWhjLEDLn~qeb=lY;lHraHdD;*&vJ(W}Xfp$_i%41nv3@lty4tgqSCA1I z!NsoZ*WbuF0+P8bi&6I4CC%AUXb+^~#iIjqpp~261-S{x+WWnrhSLjBA_xw>d$qFr z?bS(7_|<@2P4{U#2~wcmD6r&q?`xMJzhC2rESGdyO+BO1jK0EbbxavgwFX<@eP|b1 zQ7FUzccvvD&)jCRsbw{k{Y!J%g+b75gSfK6F@uDDbt~{7fRw&z+&3^lHr4ma7DG*q zTt6N113v6h(QaYrjV^WtLQOF)20?zFcOh2^8ok+3%SIEUdGLGq@~(agRn*bS;x^6P zLhRy{SC6rZ==<6Pc#y)mnL2k@4yB8DWVPi`d*F`Ym9?H2kM*r@Q_Y=LOBWC-v8B>q zwAaHSD&DJdS7?4laZmmG(xDQ*D%9emB9G3WiHy%kUF-+w`#>}`lFC1~B14xKe4hbu zAP}B1I~B6?G}TwyG992$7$S6}M7mTjG6xSmSA+Ld=9)q^CS8|z6`j9itg_KdBzmKy zDWbvqHp93w;o;CqFz5fhBRRLD)Tp>?DIO{_sHpXwgs4BE)DD6)VcMKwE3IHSG>Z9_LShC~hw3uw!k}L$qzaDtHB>1Om8&x)vHKLJ5*T? z*uiKu9X2{6Kz7FCZsjXetS+SQ<{=l3xR9J3S}d^Hpax zO?OR~)y^X=0JRg5+*RH z0o*FgV*1wqf~b-j9_Cs}(bVLps3L`5o9N@|U`>b_(7Ox}i7`I6pjXOyVa7NAWj3P9 z(Jn$urM0xAnbJyRXQ={D3?>PgxURg`=W9&BFTla5s}%_~-}nV6>~>*Cq4HNU{iRf| z>z;QNDx~qK7&eBB;nHj&W-l7RC5sU228}>~A_VtcDz7sw`1HjVi$UR`3X1QeOVyUo zry?Q#D)BV|gz}BrK9%COCFhQLM5?RPu71JYDO27vjiY(l{#^-Ci?P}QStV-MqU0tq@iUK5M zPxPCaTr1X$WN)T`J<)sx*3T=2t(V+h&Tdnjkl19zpwO7&+EU-_OU=G!Y7MfF;lc1p zSZDo*ouO2p4_JJri!NWu6~lXOEXwHu!{e zk_F`E73i$$x8Mr|4U4*A&#kn)`bW781$1! zba~vxo9B$KAu+i+q;-n7q|K~+^(Hxiiyb8Mp7%MHGKE**oL6nE_suFXv2jAJ8q8t_ z2J8a~94F?kMptXvSk_*TTv>bCr|U-ZW>-$vo9-6?%$tQMXc~?n@S1iC#bI}8gQT_O z0mpl2VxKgCYfScb>G*3T%$Twb)(S*q#``~LI@-iD?!T)8q73?a%JrMv9>vLj%l8x* zDQg|f?3)6yb7!&?)nKg*`S?*_gw_yCvZpbSX@FXTP=v%hLZ~#!2?VF*#z*jvdr$TSYDYgW$X$uhD zQ{-h4Zb@^3dj0gp?t%Ly;2t?$E1-&juB;3Yc^^Fm^6Y15x{M%-!VOq{7cKTGbwncv z-b~z*MDXzQ=cQ&(dO<>kr4a6}H4szsmHSbg{5q+Ly-ERm4G`-YNd-a>%eeu1h=8F+ zGQ;=LzRT)M0@pz47eKsa?7iaL%I+A4Fv$=8=Mlcbk564^TZby?S#FHv=X%pxF4crS z|MpR%gM|R)uX$v!h7G$WQdO6;^Ot*6OI&*jV+f~E4*hv}r1+K9AEY{1j;pIU41y<~RwfC(;o z=UslByAq{l4k%t{<(gV(t2-QXZpZIw*(}vnDQ}Rk3a~*^duL`@d~qh zv_L@81wT4c;{;l@CGT)Z}z-ZOw~?TAS{d1a$>0pFgjOj(ZQT zgH`y>$B+aGrFz?e>k&?N@cSs;JZ)igwe~TrGWUbJ^A~#Pp0=X6yRN^O6c~cpMbR*w z_K{N;lE%|Iys|s20}|iWcn&DFa&wc&SoyxL4~e}!f+oTdk}{F(vQ<_|t^Av-?|*5( zX{g%d%4gptfOWFfdr>DgD=@O=mwm3y#&>C6PpKgx0WUVsuT$WK%Pyx+VLnBl47c`# zfDJHD2XT{fmCm#Cvt&BCD%Pb-5XTd5xj=09yTNYpU3YCSrtkxl^=L*78sa@J!O`mMuC5^}d{Pis>&_tS zsmS8M3fT|Bkd8)>awR6P(1k)$Rxv!PVrt+h)zJd6PWLj@U%Hr&I)zI7XFTaQ}u z@RsGVe;>!xn*ab0KD-e1-NWxSHN*;oHEoiofm<3028z~2wHJtFR1oTDr*j6>o$&JU zqs#fp$&87jB_DAEq~Wx6yL^B`~5Wb4!>`{GFnYHE} ztC3vBJFLL^fe~-eDOQ(z-EB}IpT6W%E~g{3%p8Lfz-i>X?p{NS4}fCdQ@~-1zSljU zeI%5J0G2A#n+hYO8Dyl;0R;RAM4sVqyj?sR0U%AfbK$Y?yx8qu|p)a%ZuM&StfxS45qr z?Ip4ma^$u;?x|q=O4z+;!4OEJbmS+=dewNM)gtU`Q_+)Gi|&p~T%}TFb|h?>K4>Gc+R>y;VdTK$Z|@^V;HP^K^*)>%Fj* zIz6|cj}8~b-ea279)1%xb+PP$)R3xUg;=?4%Eh(wMiQGx5RFOWBY;4Cs!`>Fv~fUH zE#Ud=mC%xOWPCJR!j$ELM|X4qCdp7w6FaP@91GD%aaP9i&vz7~?EvNCM!vvnZ3R9+ zZ&CvPx6K5e?!)keLP8g05oRUfB3@AJ%{=;bs;dT6o8cMRbmHxgrbp*+E9H7 zkI}iMdYrG~;{wTMiahN_m_k5i$Siy#%pPwkt`gFoObBeP1YCys`_ePxO&>eiDAFm5 zz#%-|$6zDUK2O8vXfUxg67Xf4+T{2(3KPrOid_}?O#djma#OZ~q^qLhqiHSaYLujr zc6A!yWIgfh;-y35b{3^xwdU-Ad$fe%EKkcA$>eKAF(${wa=eKETX8Xu^NA ztv%Xv5Z}9ar$uk?o7jr>ZFql)_`CJ&|IYY}WB<}z{yX|N^}m24|AXlN^)`}cex>GTq|I=nMd(e~T_67YD|MJf!96BiSN5^5}yu+O8d;5Q~;mt1v z2i@z>vi~^$^76<(V*elyfG?T;M#9|3sr=`TME~@{|7SX*qwdMM!NiosJuO#U`B<;O zx(PWY4*16O`r%&A-+^&e4sODKynhVaD1-HnjYE z{Ez8>!fx*Mk%Mpm3;)mRF7Ia?JQjQDhiGz2+Ii*_uauG_F5Yp@oN1nfa|xSra^km( z^L9jh>vkUo=)8)f+mtGQXc*6l`x&()U~HBx`+X`b(vjon=t>Z0D5%!h?$=h!qmX~b zm|$ZfCC{dKCO5ABEO>dqXi~i&dvTQhcQK1Y9Hf?oyIWdYDluxJ_J@Lkvl}lZxhD?P zuO#32Pg!fNsQcax%nsZ64~}G4E;;<#tTa)RsrDP^VSZz4zp`hOd>+(}OD|#$#fuP> z2kTeP?(=9dH7EA-EqvKJzRx4TWUo7q+<5Tw$vzLuLutYFrXYpToBKQ`-f^7|+!t!I zUs#i=8TCiuu(Ie${)hH@^Klyn zeUFOU{ka1#6cU14k7%#^|WDA(Oe(5!EZvmT^Dq2f0 zu}kPy6-!Z3_JqEq_{!@ukKpllolz|eQ%r6#ut_}GT5F~Z)QxVfMLRlmJ3tjPnyk7D zJYL}n`JP)1GM^Bx8X*ARnCcoIUXv3|AIS3>SvplI%!6ujI1J6J>hIDh>FS4=HsUZ= zY=nD&%`Y4|M;-CFIBd=HdCDV^{_?8^qI26_E-F7<(@9h$C#0{klWVd{t$-(>WjtLnr5@nCg z5{@Jbgf9DBt88TPdVdx7ZdDhe!rKBA?7j(jpxroy*m|WL<5n)WeV=aBZrZNP4BS`? z?y426&q6ay3cS$AUW>OV+syJN15;7l7NpfFgcBx#b7Umf(JR=N4JcIn+Ong9Xo`nc zP$g^4*UPf4e-+0T*`GWmf_js;8Of~zDq6aryfd|!YUehb+;aa#^o<6_^PE#DXcxR)$wdg)&|B!I&pg-h;dfn11@$HaOL)mXB0^1JZ(p=!1^XCd z;6A1ZdjnN4s(Dl7EOS-sit%rwr(G}1ju#DW}z@V)FZ*$&g-q6V1p&-TadZoDs3~^a7Fj2xSxwTB$Qp8iz3&}8w;Bc7N zi2+cM>?;7d)ffOqP-^tsu? zaAycd*J9Yk9s^efPVW)8M!OC#A(DlbL4>s8f>IuPN}5^YaOQnxX5d~RE=d>e)y1(4 z8ok;$VdWB`jNh;+;Bn${*8V{67GR+be8Ycy2wWXw`uJ8UphUUS;po$B@T1Ry#X8oLo{dNt0Mp{pCQv=w zIH>NPi!qEx<|+)eqENz~{6PDU(N7`tg_jEFg#H}*k#jrsyH;!%^v5nw-czGH+gmt7e&a!4p!(`LYKnwfG{$o)7@*w|URE#=4J2xi6z$f5YG#VSvhUYH4 zbuP)(l{v_1*T};H$6(`n#7s}}BR=>_xi7Vh-yj!d>{zg0?^2sQjM`xO`Q_W}J&w>%^n zW_D@tm3@sG?$?FSrB1grkJ@i+4*=!_&1t^{s>B&J+)_AF0Yet=49=4y;;>HJ)7NUL zgqY3Q#xSa4c!>Qb^B|2jX#o%ptXZ<5m?+37bf{)+k0ny(Ta+?Me24Qw6Bn)6kCZzZ z@`Y3_QQa=C-zyo#_TTl$?)|W9mycxRIv)W$8}c2kile%5t_RkPM$O!nL4N!Ys;zeX z($}>x2RfYr!V@>zNh%8#9L6Ceh->TRCa)n^RRm+_28w)&7v~>rSNp949x{%Y7*kvnAzva$lSnJV|`UaZ09-(j8?_J4S=mgDcs z^uIy<;wn!w+9~`7Fv(aK3GMmLe%88%p@;t=VQ*W@zw{GG5do}~i!Bzs6>&3`_PRUA z1v95yzi9l`Igal_hIS!+QcEhF)wFKvN2gU(7+c%-%g2odc%t0ds5*{ZT5ZYCZmx4WH{qb z?Mc^XNZ%{Y%=lN(1IkUbKPjPb`%2wMMw(RuijJt2m(vmfy32ll#hckL#*0K?0iNLN z6s`WoGOg!PnHPJfmeqMHf4)D>bm!oH?CY?HUi@ifU?l!y$FO~tJ?-;P-Sf`}>Y(h2 zEHUY34z*23%TWgMiN|lhGr^B9^n6cl2T+&Benc>S(GUMYo(if9TjXK-aB!mpf%r=3A830b=`RgIyoqyj^-w<^TTi$dG$))(_)xaKRtF zlD8`8K>Oa4#rfdSvpajlzW&cKYl*}ls-$Bz0}Df0g=PR%j87)60Zh8Nkr3M6R95`! z4JO$=?*9`0yOD>(=}l=K2PN!fU8G~x@nP-F#MN^*_P%hkSFCgB`9IpWk~4!St;YE$IpiGqlBWp)UcPEz6s7Q=CwwI2WQuDCS?iYhfKs1+b3UJBeBXkEQ$D)$UIW_Q$=xIvskz=VOd#Iwb^Bj zZSIu#nvvVJG=A;sY3t%8pyU(b$Ud>BCopwzBa(<43c(+`Q90*8{dw`3k$0i^ur|+} z%$+%Ul3TTf85k$+C5E(%G`NwF;WR&xe+L;v~^qZa6GKBAUCQT(z||Hsmxl zY%DyR#~y#VmQ7JC)P^Kg?9>jnRzD_|zp%cX09AYti_nQ8*m`>XJlom5(ZRnqrc#e^ z>2D)+^qV#&wIE{cL-S+t-I*ZPZf!}K9YLF(nc+!KqlAqMQ*U@~B)4Tuq$Ox2ya!$I zZvBLHIMo6&WzI5 znD_q0Y=V=jmdNw~Ydn(6X3~wQRRZl=`hJnKNoS)Z7OL3xRKoy^S*P~UiTw_~xV%%E zC3ld?b9}OpsM+{V6w-?`w)ZWF)dXut)MtQDmG+SM0I;Gy?o;9X_0?Ec{$9L*tx;TX zY~)+0BovVA)iWADqN)~bA`OXM@UqwZP-4m)J^GjsVWyR!DpCQ2AQUZ&qA=|;)DTIONb3Jsro4os~LA_Ro!lwDGWTV=x{uShWQEZDRzX&T37+!pktgXCnxHOA+;JMS-~Y>NPry zC3o2q7zBa%uYyhc2CM-U5v1H|*HMAwt^jAi;)z+1g&;mQ&JGg9Vm)QxX^s4{!t(_c zU#i`>(fB(PsFkAO_ddv=w$Wm?xLQ-+jSQPx=uOC@te54A3DiSOuMmt7&oh4*zuw5M zeLP7gmQV>^*0cQR?lya~lZmt`EyA;Wye4HV$Jg{ku)T%1U%)nViRqxz66;#LE0S`1 zX9Vb0pb@1VXw5P_0@#*ZF^z?}8lkI3UXq&U!}$bPG=`eZ$%4+-MR8w}G{6R{bme0M zVYIvw+ZhHDRRh=u_PB`b@-v79UJaX_g+UBc#W&jQvJ=aUAgcyb)&MV0k}>b9OvSHp zmm@4A&npN7mr4mUFmr6*pLv!?M}_vg;*WJI6*$^sEm+{*mVGMMs&)ycsszQ29PNb$ zSJHg~jK5Sz?6qu1wW3u>ey*-SB!~8Finu4k$l)*M5vf>sR1BtWyEC$_T-|3j>?TX> zrHC3^=o`Gz6|*v$`V@%)ja-b1N;MLDufS1tZDpg|@)CSVxdtO>8Hb}y6^^Yvm7e@) zq1$Iw-q*wZbyA4x5|$1vte==^vRC6SGOSasI0ajy*=KCJ&9eoz1$Qujk$CW^nl@+rP zTiqAmxoA=8XF`haFDBBhM6yL4lEFGK{wg1Apz~9R@mY_Ndvn6Lx*a~T=^%l0@|SX* z^?Os?TY{))WH1ZFfDZkIM&z@4?Itwhz|(%JurvNkyv~ji=L`ZS`m_UD`6K#1mNra8 z4p!5tQRw@_Ab9C8U7wHl^!W3scpA29GPYG%`K-mgA?DH8ZDmUTpggEg-9MRAFhMra zD`mn&pfs(~G%g@0@sff*&)4{_)+T8Nk}bgiAXW40aqn$CgB0aU+9`uyV@an_CGev7 z*v5jcQ}L=NihWf@P5~J@%OCRrzdi^l4+~I}dGGmaMck6e^yO!2!!4UBs}d<9KLS)& zb{(D^e9WY8GbinT)eZq7BUAJyTV)cY5b?SThO%8Efnq70m)X! zgXypNuLA+MUR4kHgc+$($F|SK#Bf?zQYM!)Lr=YH0^Egg{4wah%wXN0P-w1Oc)61v zc7Binx%bDoO*RFQ8`3yotb6`TcMT7Yt2+z#T^QV~d)LgB?Lx;J#zxwRrrR;&(^b+) zjm~8O3(07wbf<~%QlnW0BF)qn?qXmgV}q&hi78HeLNEe0&`c>e5-NEw@4VK`lLwJc z-(zw_P?E~3L*^KG1ZG83b_eG7Lfp-{x{Dnux}-v_@Tk~Uiu{5L9<>2SXw@E#i`pQ1 zR$cQNp&`Wbim>TUo1V_j_VKW%9S_nbgKA({H=<{YY?+(&deCp)QPWM?9~nDH{CXVK zo>RW#ZYmqpuNALV3b>X|UA&U&?3p&L1Ah|apB=%RqGA``P4ojGAzCwfME~54bFbGw ze{SdRH7QWPI0C=)YQcKdIWZxP%QwEWT9qhDQL8aai>!fo3SUJ+?Ep2o_b-~k%2u`4 zl@WY`($+%esJKYbz4~|t7{kSY1&%gB-$ryAHhVqMZ`)E^gEG^=%jk6=sbaIi%@VTq z@RT=pMi-SRscn~X)|@UHK<*ilo}EMRqwJ}GT8zEB8;IxWyKe#NHMe&xyo>Uv?tYRz z#YUOmI{8WDU$KisHmmlSIa>xeF$g{wjI%JwzScfS6?0mtU~5Viwx!V;KamqvLf1LQ?LB-X?}i0&`6~- z&k(lY2TF*IU6hY)#i)^VHYo7~f!`w2%}&+-_E@17OH@+_ zk#mewlux!puY((mV$uVIMoZF12;Cp+tx%JZ0}oGmCm0(v^~W&0lQzP3t#;8DkJ!sJ zNz&s!Px_{yEf=JOjCz@@@+U;lWh z*HqIp=_@=lMuVv+FC08{=|?LvLn{m#r}cUw=i>F~xi(e_pgXJ@8i8nFxs8044 zrmu{xqEl7y+57A5utv>BLsp7jmYy+&5`tFO(*zcq7FZJ z3s2MSN;DR%u8^@p4Vg0@hqv*{=RSq_ckXmD-tJR%w7c(8PBq%vpJ;YK!iF!sYNksW z5C?4ZvWnmiZ>1dtqiHx(Akhkw^qsM{W0%)^gWKo7*w<2Lvs?AhhLsVniaWgRy$!8) zQLH$(7+gWHnwiyIN?f=4u@p4Nbq-;LGvCFNX*(6Zw>#zn@jxxBLPK>RQGo%0o%731 z>v-!d4+mw9?Yl1?{B73rn+;c|q~JOE@fI@51qVO2%D9e&HfrW^Hkl477kL{x@Tebk zO8_NTt{oc{?lNSs{|f)3+N&+(EZ0iuh~t{;AxE`fIEaSQNG5B9SKFg|Q_5n6E;VN9 zIcIv%S?Szwp-7p|$UA4gOS_fILbXgxN-yCg%A7jqgrAu9i)D)?oN*q6g11QL>T#Fy z!pY{T1`pKV@qE)2>ihqD@Bg{p+X;v1GkWi_*Ma9q`m86BCk)fu&S=&k@T{s(jb4lK zc$SM)UhCB9i0kXTadBq!MYnu20*V1&{&IIFC*nE>)@iCRP;O64U}@JDmZnS=BuZw# zV*>v#^nEaw@BYVzcs-QOxL+HwB%-WP90hnPpAIfvH zDk$_={-GvJ?^N&9Xe<5jOBV)ax3b6G3hkTfK|vThx91tx#Zh)fAGH>ToUlY%cVS@4}Ep91Z)Utt;7X>wX(;=+9o=)TTT06mWL9R=_iW2gtdUCYIlJ4 zbo9BxX=U&$6$ms5mJkKWW_ImiRqwau(@9UEUuuWuRGmq9hL#||DmSrmaxaz6MsW|I z?#6a%k+>N^*I4WWqu`#$QwqG_Pd^DLUn{Qg#WjRbR zYHE&88C|hsdM`=$z*dg}7UjA8C@2%MQ+{`B@1`W=!Sm;tK`&a~6Z>03Qeat+Yq|#e zyfGtka{T$vi;5;gfg)^1J-;x`NzT>fv%4w@?X;VlJ}!_|3_EJ9FTPeO<=<YK|LL zUbpEqwkslNTcR;OcH=q%2ytu`6wZ@0F%TW+L3s4SLGB5nSox-V`RCDjr3MM?#bT}Y z9xn2I?$kCA`v@mh3^jh|-5<_>jHM_Xmw!0!u7o-~8M)+oiF)&h;x5(rZC%>?LUe391E|0Jij4*YUB3K$IB}WbNpvf{HNe=R&zx~$?29_HD&sT zGv^qqkE;YV)*jRyHbv&;Jn<~iHfT?aa^im^&n)7QcjN34_Mcy?|!&X3n9^5Vciu`h(Dt%P+%+0bdqAa*N3dU9u-^-8{%WIQ(%Zi%&-tjbuFGQBMi#(Tb<_3=hR)oHhy;i}D@`=$$PCDyK#bE4-kC|^F}xj?ouI|RZjOOb)N zjqwO*$!Q5>G+4C}O{vOf5O=6@5yR?ygk>vDUR|C-q#Q}_J_oTwJNIB7T2YFZ=@8q2 zRu&XZx7d8~L~5w+YyNC4B`4s7^bT%!<FtN23#ES!O;_d3!w#ksp4+yHKn2XkrxFvl9H_BBmmw zIC;7?y-+JjS`KLb$^xWGx^$@MfVqaC3b6a?_9$8dH#ob?kJt6NcLX7|3NqTOjS zBmEsKX*mW2;PB4Y)E%r?oFZZ>du0(=z3YCR+S;TN|4IWd$ZGAh?Qq+f374Eyq^7Vv zdQArs63g|Th*zr}Z8hC0D2t)^FJR4+O@-sE2{R;;biw);k*j6WF;zn*-_GbjwTmhC zA4I%blqR~z)s9xA*-6cvd_q^Y?>;tX%71ejXZcn7or{UN)13W1qR5(zfS_kp5Q}#a z|1_(W_*tZ`oMS#bWOK9)ooMJZ)>Na+A-Nvo9KwtM+so7n#|cNj(XwrxT6&L9ggs$- zZ$%h>n>;KST7UOuyEpNs6%&=cvOP;xvSoj)LkgIb3ZM;gbmJECxWPD{-1c+?$Ebut zglG;{^J;ZLpbG3lSBhoWsDMPaN4keaULCA_NLL4{qq zHM4OC!mZ?VD5ww~KG}|zqcls2TT01k(UJt`d2!42+Me2*6nEiQapmu3E1<6qNg~L6 z*WQqe9`!<5_454=@Yq~{4eCp&sfOW~bomR-1BV+<&zxv7?N-3|Fk0xU7SCN;({N*y z`&qGF;D8r)&IFZ0K4$H&NOQZ)d?WdZ_T-H(vWASKAIdw851m;ydp4?WA+bDEJ5pnZ ztSMP27-3tm4cXm9;Q-5dv|9 zdN_32vs@n-&UYY28+LMH9PAIWIb#B3v5# zHH2&;Z0qty0>foXZRD|Ul9XO)5nOlwL`rWii&uB7oHgr{O1-fPOkJxiQ+c%-LL`oH(E-x^VnQ!Mz-jPgKmP@d?`$%}y zNhAj=wI%;4+vUw?-{6bF}fT->m5fOY0AydPb8UYRR;7a0IKS` z1LuLG%#6e_TXH$w8aw70k%-NN%~@T1ej!poMqWRP3^S38=Yq7HHh+5oYCc=5YiK&J zctZ!tC-M_?(r(bTk7^Sm)y?}s=|;>w1UIFN1$hkDa;wijI&aLezRegS_?%SMM=!=2 zCP64b_%UCMk}rokAz5wIwLFz6I;+C`IXPHQ|MnNN3}`|c?75Z1sE5`-ZH>o+Atf&a z1&)!B@Pdf>Fu$l^xNRAh_39YH4*8rG)+g~PSX|6$rjbTH*1mZz%A9&(L6jbWlDO=H z>9FIsY}=x!4$ixAtD}$1Wnd(07t_?Vx}GtKtnh-E$ugH<^MQQZYNpxCrnuQgX5;&6 zERO3)hY>3)10cIgA2F_>O`o;@!WHk#)>O2m0C@0{a=^-)^~Fj?71?1z+O26HB% zbm>5$YAeYB2`(;>hR}(DY;ee`)v`?okXnS&*S2m;=`~hANVbhp7Vl#U)j-~off2*1 z(&nTo;yZuETt^flm01WRa@$`YIha#R3*&BA=v?Ruf2v`YMby#f&qZIpyFW{r*yNe7 z9%{mzRt^a)>%8qk+;3@i#3HXy_Y%DeOoj6CrYqa=?!3f2b6-tUMU#hjyjSW-Q}cIk z#}VOiT`g3r5EmmYotDQ2B#9!~z~&KG8q(zQ{7^Y}09c-!THbhCGIdO~jW0?7byt9# zR2|0qnvZ+B)K?X4DTxYpn#-=}H`O_aPy_3VglNSZpIM*fdNrRFN_TtV%1R_mc_S3& zRS12$B_As&=(#SXiMKXEmyW33lP#{wSVnR&yH|3m{sm}qTF0J9Y(t4#85Im5741)H-=FKb||EvI3r`n__G z%0XWqwhX<|1iG#j!f|28;r_=Gt#F%Y&dJF9`m(wmY^kOuGgxayp~B`b){F0{&)>C+@AS#nr9&jMgro} z8Bg+(^sqF3Vq-AH>n$E)Upd&OdK+7k`xQ(Yi0h+ShSl^dPd8nE=2*iZg}!c0QzP=6 zt#M{q2(5k{{_c~yrUimopLCv>5?_n360|1FI^dCIF~_n;OEjGLgo?be%9vRo`A^;2 zMc5Fczu_BkK0iWq^NoGPfM3&JQJ{KVBuslPP+04QGK{IX)|{02P>Yx$gw?`ZO@$Q( zP$||xw(Ti#N4`g$g}-fHxGOn{Fx zFR=}wsyTyO(P5nm$c|OvqyXk+JZq`&3- z8zvvG$Za1$w@jW65e&}yN+R*L-JF{?)my@Vtt$hnhidiD9s0G%?z?e>0O+i2=Q`Gl zw%UyVRd(gtiFzOvO3icrE5MTctJbcbdKgHQ^2?;(5xXUJ`F7^(a?=N(8?{J7bw16x z`K)_##U3;2mkPfNT;LCJldl_M(5FZ+t3XHvSYM z{0vf2lbxftX8^L@%srD|(In&&{V96MK0Hww3grcyEcFq5XCB@DI93cg1D^x;{$WKTtPLsBrKt)c|7k^pA0F2gjJVG-zr@}aDsPr`h7FxA@{Ls zZT@>FK&{|8G@r+vyp;ANdjk%Ay)7y#f(ezcg+ip*yho_G=?>#&;K1*g z4?_6eyZoQZK1|wLa@}Uo?K?xs({_xhnaFtc6NR|28ENQ!&xLJgwWa`tXk1O7IbnDZ zfmkXE4LnLmmoc8i}cBunA8CVjZO*u zAk^nCZ6#BsP=S=*{%j-+*jLKM#KkhLJ1=CBbG_$yR;k?qI5NPK8<7#3d}u;!bb)-D zFEaQp=enf?*IIxzG1^0hu_js_$@7gf15eDh2<#iNg3>Ka<7n|@_D<(yz}qRxD~$|) zg3t1Q94(rvYod_NRoK2HWpq7d3P0KFaQ1-DvnmTIT-s%W_5nPT@)SPnc`=PA=@ttM zyKH3(kjEY*%DbO^WbO9O>UKh^opd~P{w;&nFm2#dkS_KBq=6B36}ALpEH+NjY1uIx zpW%)1%Esfj+BVQ!V)pstw3vmzWPRrrbuf^P&*zSR>adhG27#hQVL>^c823TqdwR)( zU&yjwQ7OP{{8wNAa4pmmn3GxRxw!HNyK@%6?i}3B30G1s(S9t0Sqoid9TwSn32g5oL1MSJfC!(%1 z2AAJXd6^GrHA2~0JVrU6vLruol72y&BcikVa!*rxWdL(aJc3=Ou|Bhg3SR#JM)+vq zlvB&uf}b;Z=1O}fqCD1knl9BGU2kWK%rJ*Ri_gjyf6_t1+F#Xl#~XTp$OPmp-u!7ufgWC+LE^J`#FkGub{+J7d8_khL+}t(9>>}y|H&I*y$cwhZl|@=5U>v38TBGNToHPj zH!=kBNpxt~`J;HII!dLX8Lkr7I&F{>t%WeS$FB56=9LM7Jnu-6LH2htw0> z%qp*zM9@A-TP#z{w<3m2j0#8?b>0myACWgArd>ip8dK8~4C)$iMa^WziBFNW3@_N_ zxEQz;IbSN&xeoZwFQpH?sMZ*^5y5jMQWW%g+n^+sJ8z5Pl-0kv=hYww+bAg=rX8cp z-k$U;j5`y!_EyuZETu#T(L>sTIPk&q4DxG*_1+Rqp}&mE;V`6)14S+8w1Bk zTc)&2co`$^#VQ^L`&5m&Q^-&JjID=MG8l1))=`var*i(K3uDskih6U-lGEb@ZPD(1 z(OoPoN&*5HE!lx;yL-9$!D15GZZlGi{ADY&xw8)~3lXMUI1lI7RjEu7Xl@l+a`+*0 z^;^ChVHv4v(iG0y+ zZjJNKx>bk<6B}tPN1p-G@i&MOz|kGpGddc-`5f?QDI3^Fh6xz2e*uyOcA(mRnIT;! zuI#S&d$EQRhXDK?z8-aI^x8i4p!Tzs5AK3M06uu%UqFrDLiZn6K;?rViQh6d3qpaW~|q ztfviS#V4BHN6Wb7r69qd*3>M!taiuae1G2nJbMs-45jVq8!B10?Qv(cBe~yqyKPmg{|{bX^^GA)K5>qNrqcNH@Zc{VqvVNZ4f9 zndC?T8-%p+9WbAZYb8u|#K}Z?eZXy;pQH?_DRM{XdHa|*R3ZUXf`s1$Oe~X)>A~^O zCh!4~zw=Y!ALf5gOd>Fm|JzZ0V!EqJ;-l+Uq4~31`%~?{6>2rD!Xs>Kbc9hbC7-;2 z&T)sY`mCd^UrSqxp{2eSAfU=Ii`Ffo{uj~QU+>ZGd_Eqd%=aT*Rgz_UkTmSPeFPH1>7@ZP%XLLvbqxlbxxz@crAjw|%)FP*_O1($B z(CW3*Li_R?QIB|&;lD1ftqkew(wrGjVX}wljKc2iH)bY}E3Aato`P6l#9u;?t)8g5 z9{zn7W=(*D9{e8LKjT368`dwSXOMY{r!(IN2xy*P-RT@EoQ+#Em=1ma)A^a{u5*_o24@3 zbF=rhv-LWagL&fd@+*CHIeMbDj;`KPJa#;`(&$C8Io8Q)vd#d-AJeR~E^_h)g586N zYgj+%vxJYKW}5C;=JDqPU*r&vp5ljsWt88EQ#V?-Bs}>Hfn>}&bGAFmt zMX%KkNC~4{UDp4KGzIaMV}xNI9QixNOkWn!aiw^4vW(tNwm4`Q$3Rq9XefM!K28UYQ!b9Ars1l@Npm+PB+BQPY=IspRjVuy)` zyyWx8W5*d4FhlrAh=5H-o;eYmgY`j~f|sX088aiMD%D-SQvO2ToMs#q$gQcH`glv{ zN^8!&xH~#dfvw@QbTvyNA850cl#HCw^_fPx^{w0 z(xE9!knmTO0EzM(Xz(X!`j@16yx$+l%jXGt1_}e&#D4yAufMOF%hgz-N zm={AR2~@n#T`JhnzQ8dro_Xr5S?$N0g4QpY)3TSMXs+~?b+TGnF}&mmT#{rxQ=umb zy_1dbniKuowB%%||3?lN*TlAkTNbWuFS2MWP@~zl$Fq;UEN~m5tOn2Dc0{|qvh*z% zC-jYInk1BH&C*1W(lJtQmx!C%>+53NyC&LayF=X-u0SS`2Oy9H5M!ebSugnZe7_C# z{=YA4v zsw17`*vrq}8kwQot--6_oVdT5y{&w1biiN2Xc)m@sa%UIYOM4Xf3wd=rvDiwCa>#h&*-gu zT2-$x586sahLflkDdZNvc@l2=72YMoKsno+A^^HI3n?_ikx_{960aKKkj$vVI2?O* zM*`i}A;l4~pjM*mp6OkpsL7ZCFZnwtnbO!fVk=9%DiKzaXAw4Xd4wR@Sw8__*P|p5 z@YE?LrA$!_ZBcfq*UHIfz(O3sP7Tk~k%0;s>ea=UW#w^1JhvG0A2P&5I=I8VeQeAN zzO)VTFzNG$W?RN~@eYzygC)LpoxE{4d2XXHH&}sO;KvrM>|T^IcOs4IrRXV83&V$C zl<$0&cP+RlyJR@2=g|D^teIT4H#%YnuouAL6H%N`rFuXt*8j^z1-ht_cbEG$){H;O zP)bJ`spogz{@`sf_oAFU`^(4?hyKhy!^8O$q#9C4Vt$rBQ0F>u3ElE>;&x&lEAL-n z$1D1bPFp#MuZMho#cN|~p3^hnMic)sYez+@y|IG8X7IY+CF*WS$LLCDueaf&I{Qa- z8;oT#4RiekMCl5@@LLi|EviC|XLDPZPP&;_0@$La2U2C}LaSY!WyXnIf}50qPFK@; zlm_qUde&@8R90c2l)lJVqeA+RnA>Rd=N2}Bhvo5)@q%&|L4gRliOs4J0Q!XGl&2{H z_^oia%DM=jB+p&^RxgyleA{#U>H+r>53HS(XRSv0!AL?6Y=S}UZ+!ml#B3aT*e7s* zYpv-Xt|-zv-OtZU=B4YHs)(JiSG7j1pb=5WF#9sODcdyQ+g7aaNXN%p-hq0-`7kP) z@WdkiiIC+NnJc^XZ3Ek$=)*3lAYR zD7jzVorCwXk4gvrlZXXmZmin$&gul0Sn-^R1scpUOg=5cwBl~LLyWCi+*n0 z0;XEawb26q+USFHn%=6GF$utvY**|UfKI=)KD}iVk-O5>cBy3!qpF#fNa4WddnTqi zW?qohS+gRhG71OU?m!hD^iA3-JUa6)XQ!m5*2pv+TdSC*!!*v4 z3t!B)%$bAs(wJxcXq0H8?gfP0n)c0t*=>|*HA z$;}eF2!M)f12BhtB-)Ja{C~{rq|zCN(doNUo34_*EE>eppP(=A)-J8U>y(E@ICrFV z9KA5~hA;5t3;SGiCPedmU9;}JXgf>y&2vUqz{Ee#%u+ej-k{i>=g?*f%Ul3p&r1%5 z3{`s0#teMrD_p4~U+0OLr|5%kee71BzRGQDQR5KO_i0*%6;wx{qbiId!qzH2#C$o* z6Bz$Ep36CUZtaDTm3DCENje43ck-KqTBKeM_tlq?uA{EuE~fdM;4a=&;>smH zZaH|tP`*^s)QNvbO1r{j_Go&XdeaL-Gof7rBrEYg;5TCZB4LsQgaGzl7I@x~^x`*D z3m{RRfc_1_I>xrfQ@%W92$h&U*2z(O9HbxCsvtGX$&1a$_>V)*aKX~uzr!B{=-O+& z-?*Eh`nq!f-}ysN(d5qAsT}o1eD4pmVo89`cfzOO+w;b^0VevY_~H82C$;g6%bnWm z#*RNhV7|`=W4ep)rW8#A6uA<^r;wrId%4XCjfvB@SITAv5TGLn!=85}rgcD=9@zgj0PJZW zf61QR0ANHiGgo&v^p63h_670NxgsthIQ~yuU9l0LpUWVVy?u^gU8D zZCSgRy$jJGAcBC$c~%ULNF=6C>q2!*!Xk>{S|7*5YLb}3@}_%*=bP+mr6A*dTe!~t zYaKyWVG|e{wA+v0DUsx1t>2M?+r0i0q{419)$B~| zlX*baZaI8TyS7H>qJH!DtE$F8zQj(_PmqHCPL9JFg_u}{!4EI|hNa8HoJ0usPUf4^ zWx8(TOi2{(8A>4A(xs0d-{}CE9{SZ?yeaXdIu(4$Ab?@#=tcYl=^JT?XqWWc$7e~$ zxt(~H8`ra>7Wc}eHMLZMZ8c{nm(dKJ z>LObM2Hcq*G3u9xn@mOwq&z~y_yvo-lV0%PIdNO>H6rMhnzM` z+EnPnRxEkGFdoLj-|VDidh7edt3ckbmTxQQX8tB`)yoLP;Q&-(N#hZ-Y+`meCn0Qz z#<|vVwzMJtf+|*mccwabi9{MvvC6SuU0s70U-+xPiT|97erE(7fq*Qv&j#Ou9Sc{z z)z7!(xagx0Mp*JqRjF_$>m5bJ5Uj`aNHrYC@boDX;fPtqqI#OUw8a7H`j%^5Vt=I< zW#Dr|>*P43iN!oT1QHKJsxiXuyk+nlYVV|Od0urN4ioWdasndwEx-a++)w;obNG)}vXvcB&FKu;ZY4 zWyVmy##J?ah?+b_B8R{Y<{-22iQdCyWgN%HAi?y~!48BJzAEc*has!^_p)kZb}a)D z^XZma)kvR=ORnsu!wiM+zbnA&$x{&#;l;}0lg@7o{IX&oUF@m>ti_icE2^W0B0B@S za*NCT8Q~K4E{=o&AUz&j3t;d=)?NvQ?|-%%9s6MKH+>2kNY)U$nzUrIk&wZ`D4l%F z6+{R4b*|O?4xrtu1?;n0|7Tdv(F*tlM+j^xV>Ggi(aKb!2|6 zbYgw0#O7#ZnFSA+|4V?^1QxGyos_|vkA4$n3bx+iUCqWBs*TJt(8?D%n95?ZeW2+1 z3lKEwEX`TACeV@^_~XM3HV|OwI+9vsRqu0Lcz*A64p=DX3X#%(y>+7W_VhQmGch?% zYv<8&+*X#Y=>|Y%MEk+Jn9>2~2XE)fLuU=o%rhm}JCYXNl4}qMWN9tDWpF^(3ceYU zt1A)&R@D>nNH(&bI-`|N`5yJR!gYn1g@Y`O>?R6Ij3~5Qa;27m07eE@>wwqIZOn;l z-vE%afZgMaaWJM{2E?QDB?}1drRHym+;45XvKD-2CXyMeD9EUUG-OS)nQ0tr-+m2p ziC^LyydxB9p5u)H-<%qgk&Q@(PuUSjLeEJkh(wN;oQhR02baOc%b2Lg(_U}v_PrQ< zM?nk}<&y}y=YznCz5`tx;%%xu;jV>*@~$}oKJNO=M?_y_qgdr6ev0c@`)u;-N2-__ zq~$XgaH&u+mC>u{YNQ13KDa@g9QGnpKdD3!&wSB!0DGN7Ib4PQ6J zRmyiTl-xjd#5LXHD}YZV&x2;9JO1eWU8@!D&$5FIMx@JfYWN&mj9ZA6SdDCCjQH}k zo%R(g%)=LhZ?Mfn&RSzsr3siC>v;D0F~9zUC+H=!Nat}ME)%I4@Fuh62c;SO8i#6z zRlna*uCCwR-j86JbO&s}Q~SoMPemuh%ecn&usT#}Rtg%gFk zoAaHtx>%2Oj6OQAZ1t7%rOVU*HrclM{@rTv`cHHPam$thD8O`fA@ksLkS$0db{g{G zC&<}&-2e#M4}G|BaHaDMkC2-V)dO&OwTqS9W5s!n;$hjP$P|qgyR#{eDw@mn9-|0IP+LgD?w{`8DsabA}bPpo_*e6)i zf0^fB-zd`wder7{#ziYWV^e~=+*PBloKqw2Pd>dF2 zOKG^J9Pkvs<-^=O{@ryrzaUuM?)u)4%>jkfR}-_--diw6N)N1Y-NIA=F4$&U#d z!1l5A1ohV|IWK_mV}pIPAK(SK3f#}&*A?)8fNP{A?5^1VYX^H|Ccmgl_QtozX7Y=~ za}?u-lXwCL0j(~(2rU;cxt6M&r@ zIJfIV)8zjJ}->m$MFaO+emekZw{56 zT=i}p_z`iMM6x(70fyR)joJd*7?9yKXWHU}1{^f1qHNBYS3@@~UQSF5>AyxiE~M>m z2=P7p$}+{k;R(AOx6^w7#P;Z>ql7&colq2!L{VR0FSJ8v&3ADGe_$fU97-vd z$Vk*neT|)r${Sk>^k%zE0mB ziB6zhz0rOb+(%9kAeYtJX`{v?)-P(jdSr6MxZK56s%`~EzPX@iM3YU_^Z#};Qb5vj z$~OVg;4k^=M!$l2bH&!k9LsWy*sxHaBoC2cp3(>tiy*&ZjdgtW=r)L< zUP_V>R?Ol?^FoAo1Ndw%LcUtONp+`*reHd(Bh{rE5~!y#WXy}RNbViRb!jh;;3mU~ ztjHj3xYOUGdBYZg@~oEgl4#SegwNe)!s73(;VcJivnUC0)60_h*DbR1iy5TE)bhHj z3FMl$N58Sriv~JYyR(pmo&7BYVmlja8^wJ^McwF)*)o#px}z10C(Swdx%dabBL2Kf zdy|F{sAeL48jIIuNCrqDQ&W}3R8zR~6jv@7Pli>R7NH&ZIyw&}%e>qpmHOYx=cn_B z-2wCW4Yhx;1``0y3eY`0_F@M9=$@s9|1zNi`spdKb#>7E8GZWR=?VxmESVJ(^U5XP z#r2(0ButR5wDNucvfRM=Gzu4|Q|COAI9(M7lUjst957u-NCw$EF0LVbt4QCVX{q;t zkN{ZqN@FoQ`}G13*~ID#Lre|>LaVEv%eBJK%HdcniRm7YoypJ+b=ZO&SWTvnq+lk5 znS%ht^AZBH*cIs$H)W4IW(HdES0WrgnDG-9v*X3^5&0U9Hu}su^d-{0)i=o3rLr9A z&}CcS{1ZW3t?e?Pj^3MN$Sd>ZLp?1#GBvvVQ>G{ctCG9hartt85*cCGAjK2m(-?>F zQB6d!@}(F=W%HhRd?qzz>GY^=&qslH-_`bcqhl?c(@n?Zx75`(%$CN`Se4|A=Pb(@ zXG@7qjo9jn17eK_Bn?qMh7c`J(_ZqqXrX(IjBH+3eERyvk+-BYpZsUe&+o-k&+~1efcYYIu6fWlftz3DT{$gze^| z&(fSK47~{VZd55a+O}@gB3KHIoL{jA=>G(n7%ilGLyZjubD~l9QT}9h0^NFXHu&?) zSCNM{nRLku0(p6EM0-mCi3wHuy>Q&1lep8!jrLUY)WDg!zAI}OnepwcyHT7kQRKy# z>}MzoG65B%g@`V$ZJ(X+sO*4YljD;AO^OD5NdKc8_gVFV>NR(DN(!Df^@MaFv7ZG} zqI1ci6#bxy%?f$X*FTt&F%IHeZA2m&N~!rj!3bqS{~ z{CO+>TihRhqz<~qG3){k@b1wNlO*p64G8G)OQ8Bc%ASbe`p1vtSJF6>h~oAZ!Bi5E zY2XwXN?rMstpzbUg~oh)ZZfTL25!4qw92s}oc7-CkDfoC3Po8@3WMu1rK zxHSeO@OS~$?(8LLalj6P=&wpObR>t8s0e`XzW3VE19k*PRYZhNiTUvWvQWMraXTH%EWmT;OmpVyv7*cJ<11t5C?RJ6Qy~5fwyZJsMV5&eO&bEPzsaPTEK1(Ob&U zib*RNaqTb3I{KVo_jcQ$c!G1SF%H@q9>Zzu$^D6nsJQMdfaktGjHn!pld;Ih(4D>W zJkoh-J8v(69N&`GDOsdM(@sry^WkTDcO?_SX6Zl*Ym8XW#G2|uZ?q=C=bPHm5MNfwChship#jl1+d0;ixf^C;(L2KH05PpmKoV$MM}Vk+v&=@5fPLR>6>~tX|C$7fnbzc zaeoO@LI|PtE#Ya2lsPvLXxe9e_tKSA1=A%LN+>6~Fp-fwY|yTg6BT4{C-WTOBqU6= zD11nr6_METCDdB-A>nb<`N8Hv?TBbQ^RTdR9AYYiM6!F?v-cV9o4-hP7{7Mn-7e<# zcl?-K;BNfb`(G~MUF^_q&BND>OZURYP;DAr{};4WhuiMQVRf9wYK!`>vRdVOg&0h8 zjN2L;=K`|HZ_(7k$Ts^sI!1T&$4SKfqFfda zVw8YG^$YsTB=+K)(yP7$Db?*COQrLwq@;DZCcF@ynf`u}t}|GCU7fccNquYW^}LJp z<8}%Ty(y6yM|qfiS$zh#HWZ%yaHYlUUKOOCtYJ0ZuynCXgVnk3`Otlet>vop9qupF*^Kp1qO% zvfmL>{ZNCz1T0Z*>@E7G{-axE?`+F^h1FcJ*9VE+aTKlBPs!e7WUR*chc|wyzDvPl z2<%$mWW5oqHGAcBdeB$U;a=Q}vD1U?(^|gkj((n$1dLz_kg2isNlmz*goB35r7Ug-l6_Mm_X57lIkk`S%b`^ZD0E3iMA|o5QZ&R8 z?RMvbTF=aplNCBiI!W&54z&tk?`DUdQPH)>$V8!0mZVB8EmhRzt84>>&vO`7R4U^l zwGa|9`ms+3byp-e07IL2C4)ao@+`mF`0B+{bL;53qLYNnkSUnXBA{LrDbdDRgMa6@ z0?|DDQN;Oe3q(NuV?;9<>BG>I82@e)5TSsZ2Dz3RY?`5;=0zY4u75U!ra)g zszPy?cTBqo`^cz+1m1S|$kD@oP8SR+T$uywI?a=+^iXc8jGJHDSj;2S1t?0Se2vEA zZ0?ELEv&p7)xlsQ&gSK*10r^}7ct|$q+$D%n?x!}fLQ`U$=6k;^}XFE$7~0MMbo$YBp%+2cF{n*Xb;0hZ+7 z%Djm;fMuz9%vHB*Rlf2W;6B|&>pb{f^}zW**5s*7hhGGj^8aGQ1nQ{K{g<`~W)~5A zCty4LcP+dT##idG8Z;lFl=*mSs_RB`5=ksKD<#n4i%OdM0_ehGY0pc16 zxtBvL`NL5sC*4t^?b)7+CGpcRwIlz3{?mc1IAW6as?i!6&m7h?Iyb_D)fQnHWmlio z$k!}!$vO)9nl=69p_ow4>(tFAISFMuqk_*n&GN0|U%a&|%e@Q0Z$Eo}5xLru+ z=OO0qb3k;!pM@#+_e!Oibn$P{xqT40Ryzemu;%T!n88XnNaWv89lTLl_q|t1F(Q$7 z8ndZ)7ciC|^w`JqXa4nmQG@i({0eK-*`o&Wjd^rC+y!}*1s2qv;bt@@F`%`xuCyg-V_ixK)W!M$$Oc^G~Xx7%y8J~h*J&8ih9}%s zG$`m^iECG%nLA=5z^4yjOi%tej~22M_KrqC;8u>cg!JmLt@G5R+NU&?Y6o(r?v0s` zLEJIUBcn3_;Hu-vt&6NSnoExX20}$0y6Hw!AOlh@x8)JU}9y z0E@ty9xG{%@I=bP%{fyM1%-xXx;B-C9a*8=?lFau+;kdY%(St;z8cbI0#iqau5a5N$zRpz`Om1h*nr2w` zsv4RRHdV7WBl9{JtzS2KR6`l!Za&QB<|@bS^6|C&g!;U=cEV`ZG=hblmG{oyt$pyG z#SMLRL1U@9@)-P0aePIDggj1c2ovL9l(mRL@YY3Skt8a=e`)J$X53t487@r9Z`3^P z6n;i*^xMdUGmb>aj6L~=->J%UL?ti_|DAoV&65xvXS5nDWM2rRRJK*rBcVJhyKA~# z!K1$8iB7Z2}k=)h%#~GUqJ?Pt6New2vqO~t>_G_ zU@7Yz^-4^>^!PRXKtT|EP4FzwXdCy8IyuBSp6SWc=Xkzj;;!pfvV)DF2(KnOQ_3odbaQ)9=Ay>}rW_$t;|4|MM{1StE&Q;yse>+`I0UMh? z<-{t_#eY01&+kXoKB=vtNW+5P1R$A)MA(lf1G4%*OkyQ~JNCh!8~vx;RP`y0C|3=d zMSc_)e8`CBkM~Ls2s@a{IP@$gR#tGcNmbN9M1H>e)Ts7e^j^w)5X+T+-R<8OT=VYq ztgZ0x9LK%iks%wbU!>2%VhPVcJsd=09#J>(D1GjP^M&Lekx(f6eg89t*|(;MKSBLB zdPRsi&oISduQ9aN^)HYOiO-^v@!FJt2t|EKL0i-(448F1-vB#aP%?B?Ug8^<>$vs| zOeSHoH*CuEvczn4Drq~gB`rjy?rqD3W4gTh1~cHE;$GE0fQ-#_0s&;Dm@R8rj2=zl#>2a58 zbIEYIpCD2Dpbl7F?pPoX#gTIknpQr|u9T&SyzG1Ael)ioVaD}K@J4p$JKLa%gxr8U7oV`@ii0lx zSr8&&y&~9mf+L14^9AOmYsE1q$-*<}j=t8GRw_%K`SqKzzPQ^7@Kx9`qV6iJR})e? zP#G;n^^wi_gcorRIIYzeW399?5n$-Yd;*Bv7lxC0&3yzdfu*)}ph1ANVuRsh`l>3` ztBJI^DxktWm=o~LHWq%z$_1@Bcw6#WVqft<;on zl^Gv09_7)m>WvtLvtNj%=GOAJofg9i1wCG^z?aurrEgvG4}RxPdl@$u5^wWC*AA{_ z*pcOoXw4Y*tv|nYy`IQbbC=KtpukEE528*KT2XdC+C=0#KkOek&uXzc?27esL`9WyWIypx=)X z4JeQF2hSvC$d-gDID94|1Q-$1)HBdNAcwS;Ry#nmKi_W@Mtad6mQlra{2aS8Vz@7& z(%|#*{s$xh(u5Lm{$@_d=I!W}YCLZ;VB!9gT;q=#4;aMvE&pOO{j;pS*80uw6f~qW zw2+ne4sy8W2Nux?J4$Z4zaJpvbQc}BdGp!hJCE@f&7UA{>izPwZKt-;^dE2O4+}Fj zoqk-u|G~6qz41FhA0KUAKf?WjMd13!Dr3y~`#5CD8d*Nrk^1x_11Kl)|R?s`HMJQ3vuO;(dL1)5*+Ij;iC@Ej@)U zOU^3-Ij)myJA&2BtBV7L!7KL9s!Q2lfV-PdYk&3+5|w4trlaCM77sQ_^417qCgH|U z&;e~jomGWoIv=gpgngnYJ@VUn5dZCL!FQ6Q-+5Vgi`pxP2Sd9b~yo61II(NP{^Y{rj$Q}0LEC*0dU}OKvU=# zx~Li|r7k=uwZj`J$X9!to7RXxD5=-1yg4BTSg_C&B^znJT|G9=Vu_N709;xBC(i07 zgV|olKzdT@IZhy=;a6dLxN8+_`bDG^pS5Lr@5Ua#(leWK=-TE-Vny%OqRTlrkAVh> z6`Pmc_l54Sq#?KLkcSfQMQ48({Ezoptr6P09S6L9*WI`O`^ADifVcnOYX9@?pKUw4 zeS7!2dO5tRYTDlCbDVkaOF-`k=_gXG$*=C8o*r+z>U&%AZ|BS?q3HLk+lKdVyL+F> zh<$E8%ov)4Tpwv#*wpdmHKY{xPV(-OzO?lw~PPQ&f65dF0Q}REyjEz_Rkx}D7CzA56Dg?W>cX|o} z$tg0e7R5YaA*{+*>R-ygX6YNTzovet&jdG6{u%gUKMyXc<( zEQkHmqUCLTE(=UVJu7f=9qV{SY}1{CG64)ar8AlzS2xq|dg2itXEr`>-TYzC^DVI8 zzAT?={@Y4qlEC1Pv(9JVJi3318as1!Orwd80MJ;LqCmJ(SOod;R7l~r#^B|Y!+W(J zFK60LTrBEbGru8{5F%ps_5UI6y~CQ=_P%fIil7?-k*d-OO-krsBlKcOLgNC_R34naTyp#@N?bSxCbvflyS=j?O#zMuQK-ap=VaX~Vfm6>5? z&6-ty-_Q54p;?uw*2?(HNqGn9J0MUp)-=+#LjA>|FskM)%9VS6VpMLd*2MdJHMCW`h%(U#n8R| zO=lWk?%r8@>3``&?#G9-Om~~ulq)--^81YrH#c#-O#f> z=yGBe37VM921VB+?+aAjCS!`=7*4fx81MOnC|;`x7H?G&O3=PAIfFdbU+#Ke+al=ATtN0E+_2bwhhKlwTl&rZ;6 z&-Z4e5Trp+Nn3V4dbp}GFi!iXZ_+a3t5*i07H1IKW6gY7y+V1wIkzSCB(xrO{3YwQ z>*`i!;{aL_3WGP6+8MoKk5Cm0BHsI7HTT-tUVEDRs=sOO zFUirY^L8eK4-45Rs-6`9IXPPS65jh@poR77UlM0VmoOpFO{G?M7>I#UBWu3NlLNEs zLp732w)Lgrkqv7$#4I8Pp;6lFFt(*?wMB+@R>SSy%YTKq6pqGxEed|+k#R&gq5Hsq zM29#r96$rDd<4c1KLpjd{ph~*HI9ZvA_iO3F3T=D-IQ`EputTHs;N`*KKcTS0`EHK ze8vfTkFiPom6UC-eDndAC0m|rdW%KRhknHMw&(Y%yfg5$dCJ?M(>;>SdI+8~`C7V7 z0(2^+6!iU&LpH|8x~Z$+c!A$+ zBnHSIg&C4FRpQj@DMtknZMQJ2%Gf9yC)l230*Z&*ef!XLs{rM`d3+JB%d2eGoX~dA ztCUyJ{zr^eRH_*G-TDFdh%iB;{| zHAO$nu3(odaqE^Mk~#1M5y|c6@>r$jp^L3?HwB$15BJzVAK8l9fYE4lXv=1d_`DLsI}G z)ki4TJ!Q(z-Wo&QO50v-xMS|>2WH8vEi@14QJ9MBChHO@I)rGF4G0fAEdK!I!NGyX z;|V(LJ=RrfqN*QIn-!W+oAIw0rcukXtttz<1{ z4DBm(QG)n92=zS#^qo+zHC7=*fP~u{_}yL1Y>T@1w9LV*Y9cWz#6#DGLWg)%(t4?p zGtv45iDh101a@7oy6Ybk@gIKYkSs=L{T_N0ZmRD{6JE);N<*0K{^|fSRVoTsA0uKJT1U z)qtiN`T)t=qZD0@Fqck5>1^%<5*(w-+TYnzYx$yy;fQa?&yMFflQacaA?V3RcI}MS zxazB?Hy6^qWe<9*ONPhWh=y4CNH>ms*@2HqWApPWmBR9iof<_O3jz~0`?Mhl+Jpu| zy*GCNvQ>2}n7h<1jz;EI3d4&}6WwG7NfBBaNGy`YXlts~Kv(Lv?3S;p zTTtB_$5IBH$*$1u{9c`s{G6tsIgVke?EPq#5Pj!@G3|naN(6i@KU8C3drIg|bW}?i zK@O&we!<$gYKu3eSKoOwF79c$e9%rSUDn$aq%KZKsIiJ}eH+yjl@2GI{2I4t&(LBh4 z5JTcy5%LoSbdqxy>RH!aT$QXc9fCz-P{oP1ZxB~GHPy`mbULjT-s(0$HJyygkA0Yq zco4dX4GVEvU*dm~Pnnd37W+|%TAx&@pj_VqPAWOjYq|y;SGdTv%qM#Bf_Kw*+E!@{L=%mt8?$Uww&q;v7B2Qlh!G^nuvq|d z{mmrFCZ7ZDwIWYR`J=&Oyy}&=DyO{S!)hln=pr|CaNS0nHwX{q?UL(J5mi~fK2eKA zH%xv2M@XQN!8*_(YsbRD$F6{h&SP43ytln|7Zv;I5Jgk)nh8T#{cJhh{3sgCN{Fz@ z8)#UlG3qSJRz*-jP1L_pn7)aX|clMQ|wmy;KlpF8%Q zP}l&qx=ZjgggS?dP3GpAFmKpOx5K!Ne5miDih)_TOFt{4;J?7a_5v4vLBx*y4peJE zo?q$jaSNEIf7+P z1Kk&My&2C;70-=}4H2K#XDf_Ko$@BG&)$ehG8XQP~DwXV>VLd3UGJ--!$Fd zZ1tMZ9bQObd+S4)m|iovedoVzoQLeB zBQPs}zfFx?wxv5j(n%p>gEq<$>~)8vHu}shx%ovTD@_c8)uz;%+fe%X0nuONl7rRH z%#5TRo%jDo6jW#4)#q*-e#JXgOJjPcUseXxS~8Avv9DbxAwKsu+>yiDrfG^upR%~& zrfAmVWNls}tY@zmkfBmTBP{8&eb4%A&(7o>gK5Muu63eDViWqy;M(4@xUJlbdQIxX ztG5}el6UiT3At+Fq6l_SRE@K++}D^RKR(pn`^W}JjNe|EN72iXycixtYw~-unq^)Q zz#m#VVe<46eDKBot$~4&sr*V(2F?YQ!z^uR)2f5pl0Bk+0^SY-MK+8lfjucmv_Q|w z^2#+WQt}hEjKG`CwcG*mHZ|&OorN`XFIm;*uEKWBkZZXv!38mdS8aeNHZcjRVOK$iu~c%GRswi$%PK9(B;l^n{n~YyC8ks(cit&)pE?8@U|k zpwt+jkuly~A@$UDKnQZP!A}UmCf{?03K;0!?`Rsj8Ah>*9}96}hRZJ=M}IKnJ*RJ- z>Ut9dSl#9_*BH&Dt8Q?7v?D2u<3eu13205QpD+}z2EQq(5G$|qj!qSHo<4F$K`A@4 z+Gb&AP{Nc`t--+&17-;qz87ALk*^Oq@npV()sNNKwqdne_o^*ub15+G3(v^Mwmayl zCLaLgk#pdri(l`yLj)=YA<#e2r9L6dCOVJOC9{W>^FRUcH%4V&p1H*u$2Ei@(&Tw% zivNH?JiOZxj?nJ#I9wv2Z2+<)qNeim(F7iVh8TNG% zA~@+^sC%??@}7uJUH^4&&T_RWQ){;zm!hY+H6oMZMVvPr-F;1Tg@NFxwy2b;km0ao zAIoCpqi!w%cg{Su9LfodhrolK!wR%|ouli(ej>Q&chD2jYHdFDck{X{$Xt&U)m$A2 zr1B>F8qp>EBKXejMDUro>=^bLM2C#UA540UiT&nf43nW2uqM9~Lgav`_C)9BqLGnL z*yBZu#K+)kD*d2f=1A!cN}1d#Y*C+^$zT>Djowl|bhY3btVh^?Q7IxZLo?s5YH&K^ zw9cZ}DR)Zw(U=`$z43xmwo(eOGB0Er8noL4Al4$puC&*DoXV^(^_&Rptf%unQ3W@K z;odGX3|hULjxU`}6||2tstNoWSztqoBgL&_Q-XuHEtj|B3!_!?6Ea{14Y6~N_1S=s zI;|r@=XNJIei^6!!cSq3Cog714m1EDs`EcVRDDNkxZv^L{)D30DL}jW?^^F)!quxk zmUc;0ASUfPpAV85FKAZRO|M*Z8bdA5_2LoB{I`ziB$V?h&c$h) zyb=?_?u%-?AU7;_%bao%_mCRJIPP69AZ5d`O*q`nCP};~xfxU8 z7RPT)yKn7RK&~RW`C<%@3(y_1i23XiZT#Wd-du-Mj%|aKDmS6J9!NMv)MEN|TX*$) zZOF`;5S?bMj}%c{)#>KNJO&W;5EGZ=x0i36lHmX=Edj(=?FGEEqTS#M+|D3oq^QZ5 zU>oVE=~OnkkSQ%}R9JMgLu_HjTM+l&L~>E=;QHkU#)s)jozeluSx;Yf1iH=rokxcgQNvaKb8@?0lJx8nwT#G`T$oJfs^IV$*uM^!^? zoClKYvZAj))hUc4L?{vMt#-P`7jRS$FobAYEYhOlZ;u*W5M^1!=F`#AiPOV|I!@lInZZZF#+8~^7nRW@aeskZLQrFlQSn!~?YLEBMcp%=8NmOCLcGWaO zr^7)Hu%PDoQL#yDh>5*B!wUhfl2j-{4Spq^EL zYj?#9SLTNpr^pb0=+|xChaFPqqkd8yj$`~ZK7j|}`uaS|%soq8n00Dmp?k`LOB$7U z1^z1iRkqWsK7m^oRA9@6(^-O1?P!!8Ggo($GH8 zAc@mJ$zM$`;X`hM%_{=R=!4M~wEBYICf8 zE4z)h%tmQgr}lKU?l%RGy+2*}-6hb_Piw;ZAUkpSHjON;qT_K|L-y{qgnu}tt@~aa z0k|Ton2qFRF}tS*t^SHX()$miJ3l&&}f2Gc)JP3E0b%o48c9he}wg((?3>N~kY`F>Pv zE!olARnFA@{fs93UZvlx)0`Tk#jeBwi1elIeT&sVh>wme3hK7ULww|miWJ%uMs)Tw z%sdmY8LOx}@=B6Cx`GsF$+Cy7Lvm4K$fV&CCUV8)yF%{3o9^`3 zpM}pbyE*Zh(2~+duK7FUVzwxG?apI!MjXp;HI&L$VuEe*-S7QVa@|L7USibx8?|4V zvo+P|@#8#YrW4m(9$e-W@-dIVn_tUwOP=r=qaf-dX6d8QQNtq68=9RYr&LRpAlU6# zIpV{;A~%U2S{jVPHOGcJg&c>pQ_LxBeaoiq`KvY#_rneV#Y9dNmtTU_Z8-&D&ms&t z+2*UWYvS!?Pu~%B6DR%vS`A-#-tSUy^4iSevf~ql@7$h6Du6PQG+>$$=B|{Y_njlf zE_~5+ofo7A-;$d%d`3r4O;4V}Ly>A3`AJe2dJ422#&shfp#;dOR&`mCOEvOC?;X)b z;Hb4L0FH&f+?R>%Dbt}pm_EpE=Dd7<{ST&7e=u2oJAUxy*WF>EpXtx3+TCCYfvJR> zm_#up@?GIBt2l2-mG(91>t0L=rq>dx0t6*-h+g&#**QtNuPHd=B@&zhy zJ{ApHANdPNM7-%O8OfY(BWu6B$I$o*?zj*{o!O<|Q5V~heD8zzs2o2T8?gfW*8iF@ z5Hu|5laEknxxe=@buSk+_Xr?)@TSA&2a}Y6A27%bd z?omxxcR3?hk30g^c+Vm1HbK#(SweQ!N$Y=;SI$3p2?+lYfK*p(@87<d9Wg&xKBLara+ z0=?i|5j2&=-?jb4C&Ai0w`St|{a5kW9YQ?F^62~EMK6{hV0ydu(b_o#P*U`vhTkE)EkT5fg!CR(K4~MG?@yL|8FPYeWz%s~=vB}Mni-yOruA^O^`oyuiIb)k760K5m2Gh;w zCl8Lowi?^8kK6B4bd;@%X3T{U))dq%6NbsF z8PKCXs3znE4(EH*bqWrdcD77zVce6!zTJkk`pew#mA+#o5T776q7dRsJZR512gC?&qu&`RwCP)QiduP zJ$z7S-JKfs1HPbg`Py6)Y6E3>3X6bhH@mzYoN8^|o6_8qy!>r)e_i~+%m2UwGVMS1F*NHZ z?$&Tu2YLMeY?#D!1P`{E)mAbkNw^5=k>G3!{Vn+HQ~G=j^+D%`!bwd;<&E&Cdm172OM*5ua!3m%hQE&69^rmi&4q&<^myp2vmd>29iJsrKi(Y z#jC0IL0^*{#U!x~XILZt5smQB5RHaZ1{iB^MC&SFAt&1b?j{`D=Q>T08tuZ|fCjd9 z4v_h$i^P$KqUumy&Gyg$sd=+8B(3&y2GTKf>yARk&SAk!joy9pP@;|{5@@geFsS%ju4azY-sos>ev1;mE zP(g5P_?*vx-C>hToWWATAOfVX@S$i7s_?Mcd;5ICgWGG*FZ0PEgdE}u+wqHlBCT2M zJOHf-jB{gu_up`YTlfBTny=`8v_npVJx%``jUa*fr_+31_=~LM2c8jmU&N_vba@=c zOf@}Qh87cx=gO(kr&ldw{3I8b2&eR}#~Dvh6vC)>HDvU1GH`_)QRDtMM_nxv-?y?P4JK_ zd5wC<-J!T$STnw~mf6?IbjXuZZxT@Nj1uh)Oyrt@1)z)<3`0Z9_!Kz$#J0d1c^QHeZ}4m8>Gxpzr&S=NTv)@9_03 z*}fl<@J3uw*I$4i54RbG?n>L;?me20Y)gs9&L$_Ysnie}8mBTB>OV1Tnb@BHUo`-K z&i(C~dhc5mNnn34nG^MPpU+=NaQV*_ZQkhb;^g-JO1QCd{gsJk4N?sG$(j4P`Yo%# zHtj2bOhbmsR@}=-dD-u?`{ox`uHbjYkiTwhFZaIzYk>!J8~y_(hh^$t|I4xb{p}ohK0gs}CMC50)$6K#}Am&5E-y|%*0iMt1{{i{h11Ug^)*JSM8Zgxi@emVfIOQ`)t&iOR(=S0UYO8yd4AqR+wJ^$D6 zgH7tn+dZRn{O>F07OOq2cGyE0 z)Aya%@}aHe+VrDzDatX=g1j(2knOd|(nhX1LQcz>Rc@UN+-hGnDv}$VFdX!2Lfamp z>hv-G@fz()u-D;YjZU%i}s^KwE#Io%Ys(=2^_ztNMr=s805 zbiJzA6^Kk<57evhnL(}Il6`Qw;rQ(ELj=N|xBGYMg_jLTX4s&bXb;E2_}xSi1nT_? z^?X8T1Wr8!bzM`zW+Ax6-iIzwyl!&ln?k~a-;N;^&0sV$xTZyMk!w~fWID}8Mn73e zl~8muo6b?Ol1}Y33m}>U@s=}38|qa6KCTmKOJZ5lDc5qbXerm5axt_$WUc3Yv9$%!~~*ieFG_e-RW zSyiKEtJcmEeRTOu-xfU*_0FkpiJAWF8dr(_-{zsbp8|Cake&j+%s5HR9>wPT)pPBe z<}Rj{@Qv)LZLtrs0&3Dqk4+h!vDcYJ3+{DqW2*obP3rqAzb`Tl1t?DLVrh0^H0Lkb zE9{hyj%RnADwL z??P$5oZCasTsq>m!`ib;$KlP`W#a7nuhoZ5pZ*I`V>tUeOO77!WneGBhrqV?)&}A~ zUf#pt0Ba5Q4iQ)jeE9dZm#gu+oEzY1GP{`g%NLM)?{;Z9%0G|m&%@6DdDy$KoUX%v z9yaag5h`;mcYUmy9=txyw3=g3B_cUS_eCYjsS<+<+K$c+)mQ)ngHTxFvw4Ez9aqKM zkws0bCf7OSx*xq_V>)sH@O1cBW%BQ8Y0t3s(C#HNpxg@=2jY>3L-EaOS?O93$9o>_X??%vu&YtVpwU;>Z z`@Tzi7=?rTpYCSoF!{44^1l8{Uw->v5)xp25Mh(CeXY#y=L-&p0)2y7T8*;R_Iv`amwiCVI`fs^5s zduX)n&r^MBy1XKwk4Taqu%@r3qqd9hB$RgLny_O%*KF$hPBT`LeN!*$_mT*AJlx_P z?9w6Lb*i7Y&sgwQ9C?w+A`hxREOkfoJ%)zc9vY{rm6R{Dm(Ed^x((ZGghPo zCmS^z91fVr^0Yhr8!I=VcKMD{Q6ULkoCBGPASAM_Hhvi(1pV-~s0O|c|zug{SiRR&ZPxPlqQ$RmV=Vf)c^D#N(K$4hqXr9tGc2 zyy!sVB8)fTv2S$oElwB=QJCd58S&e`r~l{bhcp&pz%8*&cBpM-$*v7y8&3%RcJ#3M zA56CVzRA}3h6cM76@DV0&G;N_qP6Y#dW(&+#_#(TU;GgHQU3Y#@jsZRuP^_oZ}$5P~^_>&nb^RLo4nDUAzzpN<%>A!%zBruh@&F zdbbz1@H0QtwV&2?e*#@`zXOA7xp6dg78K$4XkCb;aGd9sDj$6feyVfP=}w}I-L_;_ z!%`mpnko!<9yZCJy)w zl9MH^5#`%~x5p+7HImK1k=aXg?sDrF?q52VG)CswT98~42TeaKw7oFc$fcyp_4sfN z?08pDz`?7(j+BWhEu}7OY-lzad>tvY?TV=JUq(K%lgshH6!9y|p9-rzp|So4KwAb6%9}C$oOA?eB%5%{FzZXSZxX$J##M`YI=m3);08yl<2e6boo=QFVG%@Lv>oUiy z!nruL@|Nv0146+iR$nB11ma6fsCR?t1seB);<=RnPxs><$!_;nBjn!%@!v6O4N20E` zD-VJA`TS;yE+?09)^j)3#&`a?mAYQqe&8~On{P?6YXTW*EQx^4IW-kjj)F&J~QNP7nkM z92g&ANue2})ExzkJ51{~%&ju?t1<;?2r{C$UX^bxSJG4~WyF#-`tp>3*p}S9#@G@c z8&VbO*!%=+__ASj27Dl^>k&wmE5n{Al2Fes{3BhUg7*Lg=EhR0L@b&9gGpGAs3w$= zk)#X4L51`j+RG}KwTj>*`tC)5li_sM)B);-NsUt6z$ z`9`4Nh!k`-Q6|)UZEU{R;iXxIqO{?H0<{c)*^lDp~x9y+cEl_q! zXREfIWDcSZ;#Iw&>CT8F<~dqJoa?lgxVI(PmxT1Wi>jv<2vU)SSzJ20UXRmhz`?V{ zcL(8BIcTeD6=bYs$AL9#zp^feF-gjfrBJBSh^e9JeuoDb`+$=hpNm>{{O@OS_HT8q z@GBO6;VDDXRk>m?HnZ*_12XqI!@LLha&J`LEO7HLd*hIMcX87c?n{`^XJ_NInKQzB zM%K%x+ni5bbk5zu#&3lkzq9FLeek_-KRnO6B7hU(E?*CYy`Y@P-Eb(s>Q=gXVq_e( zD7~uV$8138$Q6w14)?*>6s8T97G!Y8WBl*(KU=sGA8b8$xZhUEm3M4R)mpT=MwN@X z+E$IP#!ihhL^mMelkhcTDQ_+DybvLyh8`3FthqemR;OCZCArppT`4U4b;o7YO{omJ zU*}vq&!G19C)ffyw*h^+weCi7gMgJifUcr}ZQ1RO!QLQIKfT71LxW_h50%X97SX>d zU7q{n38(bNPPA2WWrM;6 z*0)$Y`a@m@vkjV(TUW{q-M%d|A!oa#wCo)Im2WIS=SgkS20HFchAr`oO$m#)(fjEK z)pzc6V!x}Md55xySCgMj+XSllix0liYMK4XZU|B#I|jZZ*MVEH`l zX3k3V8Eb0Am$H<)8dX_y)|mPm>JIUXHHH!v-nh(`qXEmj=x9g^9y@cnt}xGB1`+Em zq)4z2y#*@a7`%hEQ@+G>d#?&Uc!`N=k6x1WNkd@jXB@a7-=9jyket=ouiam;CxEBG zUfSy?{3p50K%D8oppm7pj&eqNWT0N4 z$I0o}ujEu^Caull?F=|A5Bh6%5+4WSw)or_Y8My=*$>95-8cG0v{7vlxI?z;opdOI zErd%vI%YD$Rw#GntkGA?fJHS1Yp9_UWt_-sw8RlrE95YduSix{nB!8Ni5K;+!jY0l z43j5wwWgMr(dEUQXai;NDJA_=-GVOWB~mhMs-i4T0&jd_&W-vNTX9)+YM%*%@-SEgklMMu-iLy%Wxwd*zC!5d0-;NMq zz3k3&1Djt|?}!@Zn+q0YLlhBJ-d3wt>0}1j1GmQ;hb>xsWr(zI(Y3DIbmfGL{p;|l z6fo>f77mwrXgz~I5pE`c)z8=i^ zT>tW&yix@hm$k~Y)8=#=i4B@LK@?G1XW(2X@pvOC>JffK>+vc-zuOPmsx5l1%mxP^ zA5bO4Co$KUUKNjur`v%duDa}4k6ICU+iWTRm9F{i(o%Va)mk7c*3>`bAc+=V{W|AhrtBoQKu|^8Zo^sOD-*)E zkwMzA$bhLJwC4k45_Zn!+s2PpSxZWBaKbrct-9!AYM=UJZH?ikhSBQLpOfmpRF7_z zpk;WC9Y>dKeK|^-91E;a0au2Fuyh^K>!~2n&ap1`io&XXdSd47t;s&;65rzd;%uRW zevnZAg?bDWw#|$VoRUeEY6fa`={axfiEsCmy>nYA%Kg z0O9lCkazKC`dp5VRLsJQVAy>gzWBIng0s!i#Xd{(CPM%`q1PD{PRd?hT z*7`k1gL&KhHtAKL9o*Qoww(c4eii@ke zNY{Nzk^+p{?4(?+MfuXGF5V!oX=3b|@8m_%zM!$5uv)VL8*Wp317Ct$LMh*O#@O8J z$T`ScdOKqOXD4aR-^L^=OWX!-yMv8KS2JB5eWtYaZ|N!o4)$`YD2Gj{5bzh=0y3R4 zrZ(;s;6~CVg>y^l+xVnPXo(#{vpNg@SiTQ!3QZwKtjdO468;Rg>poS;zA}bB2eeCo zC#jVbE`(@S=L`JkA1GMh#j{PjMSbnYsnq9&I5dQ{;*CXAd@>i}OOzCNJh-Xr#HDc| zs*62Um=$jqw&h2ccl&m73|?QBc_-DG_6Jj-no)52z~`BZi=g-5hRlWKn~Q4T+_CJ+ z;@W{1BzjvDG>;@$p-b8GhLaIRemPxI?hkOSm$Fj?~c89F~U@A{vZdtH& zp=cu6^lsJ%)rn;f>1~0CRfhPJA(5fu50*tRoiV2^iNDbBREkwT^ z?6ZcYkYV=#LiBfTTQv+DyGncDdA|LCQ#up;zsaqC+6G*DzdOkLZxY2Z+V{UsT>^bB z($BHCpY`|O$gLx?hzo-qZsVF~YjN>nQ>v;8$#VHi(Gs_F@oyyWnI3m`$MoS=X0$$b z5!bs@xrN_zAFlFax5%Qi^==c=K8c8tK8wu)l;po92+*3c0kIL!#3k?zHft)x&#ell zxHeeaJ^tlBN4nkwn^X=Bpt!D?(74sk(j7h*3jF@JZ2(;lKlJb^{qLe=jNQhHjKHn$ z=BCm64l*5#d-g0k+JK+h5U`QZyK0!DKiaD~&Z4ijY%A^d#3Lc9I{dk^fkVHC)Ys2X zAlp-9UhJiPJpYPx`7`#VOh=aTiwz3wd*2x*otMBM<^3y9frbC#6J%EhGwpkZ86|Gs z>a)qTyfoVj46g*2Dc6|fn$jjpUHVbdo5eYN)&i-@7dheA$Gx5et6jtlF1(t#!&;8s z8Y*!a?=;gQUFKPG6JH^wHgq%IgD;%WG%1iNkXb|C<{nk5+!pQ9Ek8->iB}Ei!A~eW z_(;sQ^7BEgUbyt~jkK){x&hy2di!ZZBS_8HRq#8Ddk7GC5N}SHQm?D_erx@7(b{dn zW3wtFq&u;eat0$={aAa9Ck{nqtM!hIOxK2NXQ+r?uD0DV80&6)ea((YUQcYCobv;% zrNxFLGjbIdzah9(u1s+0Wx^ny^+N*_a+*_e*9lm$GT-Kc(*TO22{7^Vfx!-(%dxLV z0!YSTL!<9@wdrHisaLP(12K!8h4q|4&!qkFo8O2(%<_F0A#eQR%Z9+jk{1S|S^gjg z=Bx|$mw02;jK&tWo_&SzPrRz@;<+mIDoj+k*hFtj2ukh<#+4jgyP`CPUZbIL&6yDI zP@`*Phwx&+9Ux=_fEa}?W5AyBthfS?N-TcD=~>3IP>~x=SNedmk#YBok(_s2_wy`@ zW5=rbfbCY<@@C3wb7?2{h_-rja2m?_Xyl-_iRmL+EIRdYnjN9dW?MB7v^G;E+&O#hlhMR{eI)3t^0?~}o+e3otc7<{#J;12&N{^Sc=&pi#sF4rgmFqy5hu@}t!3XdzW6ZfJ5;ba zgTJt%mJv`#T*!%=gee&w}+DU5z`!fr-~Nq;vQd_VBBJF@m?NchR0qh4~HCw@r= z|AoHW3kL^oq@U?0P2J(uUc}Hb2osZWrTWQ`kO9$4&8?ept3E-`QX_Pztv<-0+l~8auR6qkh^RhZ~=D=BRf4lZ=QT=kgkuUh(u<^yOqVQhvM&vAru57PQ{h=n2csDK65C2q?!(NNIyF%g!z6w<{uXHEX6v+>3)oGk5%Q}@EYyz z&pTRWvI4h_a^Z(NtV@Rt)Pke({CKP6M06sTEZ#K=oXtZCe((@*jBa>L2t^nc?KFr8 zOLDmT0etXBT1pL?GTgl(7b6UlQNNF-m-+RD4>mx%ywSn^gkqA8=ovD+qe+3N(sY+z zS~h52t45$I-XBbYLo~pwJ_5y%DeNm5h4FN5ojleQ+tk7g1K)f%PwiwV-Bc5D)l5-W z)heFea+LT`dm1la)@`XD6hEqP1|zQ;7iYEMBk;IARVXP8H>Z+xSdmwX!RsA{3&dk5 z+XrwSb5@nN!>NSG4w*o8@p{$wQXORhTBS%%UGsdjLPke7RNa!NrYj}aHcMTiO>VT+ z=?oKE-Yxu&F<|dSs8S?HV-#Oa94(3K8;JiVDC6_6IaQjMTiSStIha7zA}J~ubiFNf z$e4?(F@N_$rLRtM#3tA5aL?(s2@!n6?b&Attg`+=Tc^4%t3d?9HK>GuKON$yeR|Al z5lRXJ#1}S!bvkTF{!yEtq=OfJJqP{@1ex9ig4~xKd$BGKcyax8w&L&et5$#2q6z$$ zf#tsJ+MzaU{ue-yFGs@5RsF~MHNBdx`;F22U+e0ovwgprorGuS@pFE(CdAYDt;Xxs zdv1wPkEV-4JdXLBFAjc-EI9|j)U;l%iR4cj+069)HpSMm zCd;8x{YnM}4R++4_K`7*s`AyV06TC|`z^H3@cBTGy67D_&6V)lS#vxzjw*_rz+BBG z5$IxrbRvZBL+4nwdpND((5MFi~MU` zh7v(KZ3;Y{xd`(Gs*68zh-&Y{Nb;z=W@fvdI^s&|e_e^K6-Hb8f%_(n;{5Xw&2wa= zn7?|2P}+mLiKC|8JH0te7Pn_{#SzS*jBK7Rf-P^ADqvrd<>wHg(AlNd<65LQOn~0v zrD)1;-p+S`l7n++d~%kzyqfQRc#xWkDw@yFem&UwIp1Br1J5a+ZaR)%zP2R%ta_Mc zQJ9y=Jk+mr&~q=ti^ zf8+Wjd-~5NPWH*A(r@rElSi8tKR`#G0+$QG)i~vqLFsN35i|3vtZtj#CPaCn#n7V1 zAv^upCB!$?jQ%pWEgHlHR}f)!uqYJlY!lTjlMFG>MQ~oIc{_Q0q&_g^_+n|2$r2}y z6n~q5I-O%ZeaFfs4(WWtsOCaU?49F76KOHa+>6lFl7tQL$!KARnS?}&CWj9fb4->) zJ$yBm{i^Bl)lE*hHpEcYQthz8_6}&=*}#Dt0x>bj#^s`Ii0m$Ji(26WO?>5asU$&a zWcqNvit@3C-)EXB40??;f~dC?IgeFr?K(^3{c4G&n~ZxZP^<;UP*J#)b%Sgx;f$gH zk5*P&wGd4aTm6SuS?|^6Pvb!a19=En9;Iii{$|bvtbrYe;O(K`jFPs}lP27K=+~BW ztF=4v1^S+|w78axZ$1!|?77FB>XHf)B7}TG1c4)3^_fLK)X5>|nP*Iw)ruf)J7$s! zAD!@CW^bK`=BfBVy+B=Eyzg5Y&1_GMD21&;z|sv`C3*J;SMu-F08#U1#O!#@DmSdQ+^_%H=!Zdz)I2W#}~6z6d4 zKgx70#MqEkDrL1r(c?A_^9=~k?L}^%xrt~dpo)avys%qRYVAFmhVnL@gm5uzM3Tgr zgaE4k-i(xE!_OHiy|zCg!@GpN{o;r8#=H;e&Ha^k~Ix%ePdLv|7%xKn)$mR!r`BfH#RSFW%Aj_H2U`wH( z?gc}^&RFA9GoZo2=-GCtS8Gb;aX#~!bu$>EHg1rO-eLHnC2cd)mxP>&@##?MF-`5?$}ZxF}_BJTUWq-xClt6@dIt}*P8-zgD8 z0>6y4Y+ic}Vcc``v)78;$>-KZG7M7!w__L`cKqheu2_n*D}vK&jDAhg)6cLcegGCQ zc03<02Tm9_k9%ThmArzqw|TcjtN(0YwEiwkM1VSSs+)LJG;1i_iMgpP zfk9KVkpkFIFZvN|&Eo>*H;#&gq--7}PAb1>FQ#CZcgaL1mgrkhi2v&D%I`wpr>bY59xANms= zB4%LKN-$>bPzTLdJxDU);(l8r!f@kA-WIVEZ8J2rbR$L4rHj!JB{L0_E>0-vXbl{G zN!8b@5z^}BlZxd@Kk1O`F90Z!F&F%f8;gJ=-c@Y)QaJtR%@`wevQfi2A|a*$1%|N# z-DFvjdE08J(kI<=b3$f5ViL4eV8X~94R>H8#D~`){EP9P4b^Y+`JVEwVtZ~h>YBn> z!Nx2mB!eSzpO$r{V!hipY(zQvqLSyrh3jq>7NL{<{6Z$+6|`>O$L5@mCvvAz0nXOf zPWa`I`PusdVTb!4*;HCW{muC^eFs@HDii4L4ohc76N{+B+7ryR4LUgQ9~l{WN<;`J zE1oxwwVhBocVOO9;`-v%MOBikLq}eX2M5CBhRXsS5Pu@Hr>j@Q{djh)P)h+ElwHRYWI#O-Ha224WQ@3XHRi@&a~^!K(D+mps2I zf2Bk>moyIaU$!(^M%X~roKl0!N@)uLxeuEQZYdKSFm`6d9p` zrN8>EWYcSwa2e=uYU*t3tdt@+J~E|I-w6gAK|aeIAZ8UUl(J9PoWD;|cNk1m({)-R z6g=4<;o&R;Pp;Tj<4rV5_IqEctbYL77uOREAT{9-qXG`OthhAUUfGgrKgDWz_+3(!8!1_%qbig(5mgcw~U-TS{%ge20 z5Q-LO3SH+21=&O)D7%sGS4uCyxJa$*9Q?ii#o#KYHYB#(eT@Y$8nud#vg+dLp_PZt zg$moL%oeqXIFEH3Rq;5#8cY9V3^PSoC6`{6ijwrLdhPnWHiYh71IxKTp|dPXvmzAQ zx)q|`d_A5O6j+Y(B32W{YM{PL%@pW(ATCYWoZ+$+A5WM#xbhUAD0UM9Ni+sUzXLfR zC5GDwPO)6zQ4X;i8u))`dke6r)~;`ybBtr52LX{(Nf}CF2mzC!Ltp{~}d(<@gbkd4ChUZVa8sBywn4KOnR1d@Tj9l;5dOyz>H&S9D5bi^v&XXzr zrck4q#A}Taw2TP$lm@_8sSMeTuFwMYOIkZRTCC;~`^sL{xlEB&g)F;e%HoXXs6iX^ z4G-ai5t)9QvFESmWa9gTg>!J!;%mjkOI_loRZuhhibq+L52H2XS2dtNN-Am3Sx#&1 zXy@n!OG`^Poh4WeP?Cs4rN7_%{oeNu3;h0zQ0cJEf7aBo9ksx$w2gu7;F;EkF)cX? z;~(oE^l5>;WTvL2>9SsZ#10HB zgLVeROqkc&h)layTerjkz%uFQ)`EF?-`6=D`Rz&crgyZDjzA;1Bd>8o+0KJ#{@6x- z+01S0#GDIe-JnT8$5}fUlUU02!HlpmeI6dg(#rMcYqm@RE`H^L!z8*L%+66UN2tx> z*PYNE`|R8|Zq9rwCzpUjlBmd7J7uQ8tz4-;e|Lrsjl=4Vp_WkTGiJ5Xn$vu26;kCa zrOMk&*Cvb?F&!WgvxF>Dtp$U-0@ zdM6F--JgDK?;Y$%{p$XVN2RplkW*aMkRA0ozONf!gL5P2PCS_s#q5;P-{ zweiR&^>2|mE+GnBU1X+nREUb1+>;r_Yo4 z++ubDk-(38M|+vhdbDj=Cnf5LRVZ#qICw;@=$et)Tt@;j>;on$fVY(Sp$W-CWD&x9 zKbb;zo}1T`0n?lVZDH9YYsuPKnH7XWGXjM5;ayfXldarzO9OD+@<<^;iu7};-I_T@ zFk%w2k#JLf`eJ#FS2zL2Z1x7H|RG?ot?#75?#jV>DT-hcTW2QADxNHuxhIy3xT zK=D%yVF00bLdxK|ZzYG5xC6LW}ykjKeA7uE6sxXnW* zMU)VEfdC#v)Guq<+Dd)WiRV-IXi_#B)sg(VpRi4-0_iU!g!=|@JRb)n)AipXSRNO)lkSr@gxr7sqc2pWStJmKt+El;}Siz78n z4&rev!64Adt~Y`oazak13{E5UabcuQyh%=G8BKPqFQQJp|6$K|I!x}kh_c1WQ4|+5 zSEQ_?fX(ooIoR8t8!E=ODXIjolu7+Y0SvL$vrZI&&tk=WGCzEi-pk$h*Gc`QGPT!@mY4&?bvL_ ztT|j_CgM67Tu<|n!bMQ)cuqyciO7=P8*9I8g++9WaPuWHb9{hSl8Sk4w!Y-!j~@nm zZD=NzyG=bvQLW*Rn9x>mf21;1-ubTXPwTGF9>&Csd0h?1bSLsrN0W$6@^-u7!I{wk zzZ+$KeOk#wjLlx{6!6o4HPi{U@;j1%l?+&%)epZPbST`ppgS+DuuZ)_Wg+Vi1y;k* zZIHnrM7g#y1%<|KhYz7^X-3+m@Pjp%9v?H1vgI?G4%^V($n{j(;}xCO_S7 z<;uP9seb-`pS^&PJzY#W-Mq7QOP2Ro(ET{BrCG&x*A+{NclXRvpqLLv)k1r9C&XfY zrrmk^s-AkHDS!J3U;zgC&nun@sdT-)!GLz@PlRaNxiHc3JpMwqLs!v#J@7e5P|j0PtNH z_RNt_Wl{Ca(a!DkSA_uVv#sNE39z{(&943-eBF5U4B3fZ3X=6-XW3{(%vBas-gb!S zbEYW)IDhqJ*Iz_e%8w3Qy8yCDC=KVWuV>|c%)0`GHU=p9$1#%$b6zU#=OS;c(S3 z=&|HO+Gp79Fz{XIb6!r1ww~Epr~1OSQnJ@kE0I%m$GWIySyjERzYDDfoJ5%JgSNzz zk9avK@kA@&CUp;#yunDtt9;pW#BqD468m%K3MQ^PX7^5M=|go~1{|DgmSxq5Xe$KR z3|G1U$VR71&U7yHb?InPMP8(&_*RN7#(__6kj+DRGySk``XT3biTRq(jZj;2sF;vG zDFk~c7+qGISDynD%o4Gnx>qGny?VFUJ+eLLMqZ?w6j$kXrzPX{ z{Q!VesU)KktLwX5VbwQ0YK=&4d@H@FC<^;{NJA+sZa++8Pfr*jB|>QL0=*?@s`9Gj z<5ELeR%^5%-yK7};A@X;T!smdkC0DKYB*3yuJ_){&E?;|w)RzS!Mc}WznUZMgqALV z?Yh$T7XvecdUnWl8<%al0#kiD^%<2mfnVf{^dVPUzh+R4W?uhl4z5T2h67vuV(<4} zsS5h(O+@3xM>`=z-f-;Q`wPe;ku2G01+ph<)jCJDlr_o9!-lwc-mQ{pgu(f$?acfY zXWmM6Y&^S$!RO<^ifI%)7Y~`P@4BQ0&mTx9#aSCe_i=tHv{CUU&E;X=byd(2;+$^| z4nBwtup^2-Mnz-#g#{|OE&q-#rM)3|)BwurhR0NI{MPLgaK0mAiHqPO4PZt(NSV zoJ&#MF?CA66*$iUXk}sRMt0ZI8?iR}a-tKD7;7E^$HRjcKOFdr;fs4HX#ZUY{DNCY zKde<>hO8TWB7<~`Pd||DVR9(QN*JSJv8YiXmEWLut7mKDL3h{e@DWOeMoJB`Y!d)q zC)SluUrHkmI4sgrJ}U212J3vFofF~S%lK5rg!SZ1!os%F$q4*&|1)REm6a8(50Lr2 z=G-lQX$X7I%~R>&l5aS>3PSne!u2KuS`$=1uFtMb~clcS4cMjr^{s1H43 z=8~pYBN;W%q`wh_%X&cwf@sr=22AGaU@ASd8!6hkglQYxr;TMb;U)&|JMTU&aB5sB z4$wRV1CCES39ox5Jz;VIlB4JHmR)&1-SopBkBBuYHn>Pop{Hpc%DT$(q*z1>T6!vk zaMJ=wdsJs9{)gy}&AXq+CQIYA!q^`$bPKka+66P{s4F~Dkc|=bI!%EsvEMPpo3|DV-YTjzIXu;=RFVbzwntv zgRkdf6|!MiM3gTEI^Mt*-=Ik#V0i(5f)Z}r;ZJClsOv>Vv4;@*Dh$cbu}(T(7SE4C zm?c-MKX!-v-&7rRK1xc(JxDDMOVs0EzItf3an6w`$*jf9U+Ackt~i0%40V4EB8-EG zsDn?$yS|j^e^k3;^KGdC6tnj=dNr#!3{8A$vMcnNV$&?Av1V`PBEB5_^hYiz7&M1o z)YSw84o3kEcwjwx@l_8Vc)}4PV@A?LXk{@uPATgnmDV@*w&Ve@+*ecprwD9Ytn+_v zS--#X=h?)r)5{H053?_iK5ag5>0rgJV@Fc#&%fniS8992@c;Xz(F42d58ntS9L@V9 zL{H|-Jwfj?Cg|9dp=*!Q@%cjN9<@Lyg&2v5FsYPW(H z4l1>UFvQFi>J@}D1rAW+zNq-~%yQ!w;Szv_-wrc=TswzEdv)We4>#;h#GB?Wf0Pkp zE%~X9@elsA1jMXq=+319)WF*(1qUC4r5}mKYb`Kc<|cY_8keU=r~zMZz(%3+{RzZJ zk8zunJpvLtDi&7SUv~_w6F|AEG%R^RQ67^DNaP+ z&rJm`amNFZ>=D=ZYT_>j+_$SVH9_l9XO|(hc5C?Xl({f5x>!NQ z2*6WWBz@M=7scF#%gwVFibhxJXhR?nIJjgD>ONFaGUcfJuNkU6ZF2FU=3xx9bgriI z6TJ<4K9G6mZjtE|No)oy3u1=OM_5Gkxc41S%S5TYo6?ZYQeM23rz|G?NX6nh*yS6M z-i?-kzlNt@(i|pVn!4(Dw09cnXjEEDYk)}W*NJ%C&>_GhHakSgdEfZFodH(L$XSsU zdAnnY{|*%1;lnI=m}Kw~{^|KT_xI|prkI-0MV*Q?wD$2o9#ZX6j=V++W@TkFypEqK zbssXwvUhMyN4vnLbxs)vY3o>TLlg31L9=GqQNqQX30C*R^?o=uZabB7%KpbZ;p-s{JZ6cZO$1 zLlXAmJ9hDL4KP32wKpq>)ApGuvftN(mm**`s({^0y>ZaZ`X{OknAZ}s zoI#-6hqF7FF{}7vzZmSIwgkT-pM1TCS3K>z*sQvtXa5%in0r}kHvwk%{$w*hQ0)Wm ztEuu*SUlxv@2mQqRI6u}(l??$#JKL>U*#W6{0a2J%d@?cRlMrgAGL)Cwz~i_XLeJE zRsy9>>3_X{JpJ#oyi>nh$5qaqe8U1~mD9f105HnH{nWR@zT(js{Jk)Z;l0f}NHm(e zvTmp7)!!kYLH*ata4&1`Flyo}CeM>!&898B za`}ImN^xYDBMqv%EED(}n|-UKcM*Ru>H^Tw9iW;sU)i6Q9{BWVAVzskaU@{^#Uri2 zdi#hO;S>C2xKc~}cJd>Y5u~nq@JriOD#QLuJJ!-aHn6`Q#(%S;-uq3{>$__6kLeV6 z`0g_LxAT?;$9hxk%OvoBEVLEEM!vgp+==`iI$yk)YF+X52au9}{9qR$wuZVx59jn2 z_aZCs21c+4VO7)@wp6tHN1=6Nw-}8vO~~F`<8u z?oSHdzni(XDWbY@%zi26*roRs5Bn4+IeV1TDz7Q;Y1s{Ry|ID%aB1zB;!ddkZiGE; zN=rMT|7Xb{5tD0ac8;cXW953Tcx3oCKU}e*;gR0!kVycihy?W`+!O2+CpZboDtIm% zFfFokGq1~y;L0%!3C9j9iEERJBz&ZTeP0Hu-F^}9NAdzs`|#PJDmwS-{TZI!(6qom z3Vj>@c;UYrirkIBbANg_RPO&`vK0f0!^`raiMp!aTMruw=gbd2K6Ao>TV!2vBrFNd zu*~o~tONQ6F7V3jvuT3-=-qwVfZF5dDX!-}snH>tLG#ZR^Y#e2q`dXzx4ip1L**y( z8~$ZLWP0W6#HH1+y(>B8W9!*D1^RlS82G-L9PS#tecdj7e|H*q_!05f9<=LbZRhRR z>s!)q|KWBNt-d)e+O?kk9_@SVd*3%?Ag{{wQw3MreZF?q|E%zt^caIyt90y-our@* z#TJY<_XxYCfXW0&w)M@QL{0qpTGPX5O>piW3tLgzvq|xLzn}e=g8$>czxhO`Xi{)5 zf~EOUsxNqQ*W0@_AQZ=b+N{<6zPy^|7O3)ow!ZAjUZdjkVW$&=xLi7-YNhb4n@@r_ zy4m@6B-8&5n}7J8`9;D5RO#MVgU{W{{jJ77eV;P?;$X7v7Z;<*$+hFW}cK+6BmSdKh_r~D?W&ed<%OlYaI&SaEiqp#6Qx1g` z9u}8wLRJ#bFrP39xy-{YG5~n-l_!vnWcSN?KVTP##c*CS*GU8O#g{aB^NtIryg9kQ+P!S5NHZvzVsx3Sts@PK#!Zz0sNoXgoov_Y z3|sl7Z|2udIwr=LfjlKgOfMX){nXW#DB))W(Nw7hfb+6})i^XfQf47UNi@ns5+5@L zct`#ou}QXh@J5J+O@?Z1EitLA%S~mhB2zZvpa{ZPPc!Vp^JFo=5xrpGo&Dz+AC2E} zy!lV;T@|221jfuWG9NeAr(h^(`8y&@lBb=7I8z*z-3#FTH#CSh)Uj{}HCK-EyZbgz z&|~|`66#|l`H4K>0B3ExIPi`wxkJC_a4g~<7`)hTM=HHCYoDrN&P$&g$*YP+@9j1f3TK9Fi7iYhzdG;Fq#&$X3=TI4k^gH2ce5D-UvWqd2QV{G;B$ zYAnb=5D*_V+kI*_J80M7Hylx~Qj_3wmFI*SpcvNZ4mIOtH!gCS-z%jh29lKpn@tWC zJP=}~gI@?d7Z|@IA_UosA&X{ZsN8OCiJenvVODTASs1vSb`p zvzf5WdAN2dqRAAORjT{4T~YwUi8sYFhmKXQLTZ~Wo$m|g+~*B9&)_actbAe{xN!;S zSR2izVq{-1`s^(Ym=a>G?^&z3<06})xp^L+$xJl=)AK>HB=^?nkMGWMo}-s7y-@@N z*k-9-h1b&a$bC?6vi!I*aY=LlhY%}}1$FToC{WN~?DAw!<@lfb-xMvqVhjnXaVKTk zK*Y{#94-iT<%q8yC#!?09c|Rg6u=0kxz-fV#;(0_^0h28au}R%xIpi~9+(QjaI+xQ z!2Ml<`u;x8C?V%l{LdsZ#>$D94PZwNUDlPGD*8Kn-zjMuXIV>*S_h($+F7j7 z>NDTt8E^QE>K%!u=AXS@ON=vX9)C3P%)(YYVjjFF!&@-s_T%g8M2{Q@U3XIQwQIZl zUkn?oxfg79mZO6Qzb;gNJL3B`q5Uq;fV{W!yo<^3EpT_0`Z4GI`Wd@p%>8`{`}-_pPS73fU{q^bl(wF>oH)Zadp`{BrbS1Kl0TAf|SK9xh%H+2!{+B<$ zow;bR3vMYey)A>5Bewv~V~-wq9v%Dh>jNO?z{no4d*581ek)*y*nj2(uq=G%1Q6xk z&kFtboB;fApZ*sAm$g1wZCf3-Z)!>7d0^SfUkqrc;!}i2m8@nS$mhjM0A~IAr|L%3 zp;AJ(h*?BLQoAPWiyvkF7u=p7Z~hk#$J}c3>mTc~PZ1lK%OT8@J}2YrSS{Xe;r*AK z+_SHdJm+7%8b}$59k03Cp`pxk1mS=Pd{P8hOZ?D)Woo=<^YVL%zeAJ`Of3mO)ju*R zl#Twya4vRCVaew&hCSusc;13X>gcR>{}1mu8l4fGB8RN} z{$k)PujL3XyL*2K0M5KWq%t`E$>lv~-PYjLIjR^n_;=D@y8Q{lmn}yK7{EwK8GsPBw{fg=sfYibRNOf-5{5L32zAX;Mpy8_FpI}{YOgU-4?G5{u3AfOR&gp*+(b1 ze{fg#XD&;93!_%FFbcGLXQ zFOE+9^8lix?lNol_Uka@{MzZ@Ub~|=@n=Kf0)GtKUpdbax;t#Y4)*`!=sN^!|Kud& z)t{{S@5l4G#eeRRq2B*yT2LhXbDtd4H|b?!l5<%V+19gN;?@1x|9s)GRGkb?ei0GF z*g~_c@pp)D|IU`-hXtRGnf_oEH%+fS5ER9K3cCU2QHTF*(uWlMwuGJj#KSdE zFZk!gXfgL51mVRl&M_YdZ#c$TQqIbwZvKOzHx`MeyFqg9ejueMWj{wywJ$Vhz~WF8 z|NlHm)gY{RHyf9+Wh4JHzQ59$vm17Lux13edS&q!{y}r7yQKhjW8W$!RkpJ%ob-hz z|MJG-Yn7kYE}iqG{lg1j*|@PpRNT!)PJf4%il4Mp4a0nQ!(6aC6$|{OVwKs8J7K2> zAv+b@_))Pxf{b^AE>j|_^e2LUj?Sao1-n5Hz1be5H5~5$*~sg5yFvTsB=)IAv;7>1 ziiDQkAjVyCBzBjp9cq9^{x^gy{yRcyxoQ7g2ryCouhTqxL-{wv|1)XO4S5ot_1EGx!2*3 z#F#f*Yq{t;i@q$MCreK|;fRME0^DIf5^uh!yt&i3+-rA_@*4oUG!hjk z&7NfAm+QgI?~~=G4VDKhKFPbfL9&=qTouP80DHfFia?n_RC;nUDFCb5S5(8B%WwTW z&EtcDUWVui4l(qsRyrf0l$>XZKJ8=xa(g$|t?^|WeDSzw!q(a;6T@E9JWk_|TB@kC zwNr*up^MS_a}k)DP}y1-RnpWvKUOEBM>mqMl70lH`q`Z8&&*NlAeDT`ex`HfOfoAT z%VWvoaPoTw#lz=WF|LZHcH5KOUiqD}qy$03Nf9eg)`lo8F*7XK@Nlrvlp{EBZzE5H zuxu@5Y$h`Ye_i;jFZ&QL2wQ4Sd~h?@!hCR2+y367C8UT<>o>>aUTur{XI1D~LIla( zgPBySrSQW|4IVPJAxAR`6Pay$6Kh*&y|U3FiFPEE!rAFyhq)JTK2U@L)RO?m z?mnI^I`irDd-(p!Bp0tX>KoS=>8T+Blv2*vw}VRZHd4gkDFEqU{Ix?=QdPG^GK)#{ z+rbyTNMgwRtp^z~>+ik29cKYRCD8=WSzbhoyxUm(l*=G!&Cx11rzcUixw~B-My>7P zyI;6%U|(3eg!e3{3F6VJ@kmmA_Mwpbp$kPgZ-I`T6qD_mnsyzTQw-+BpKr|cEQL#C zHMnOPNE}tV))t`LTi8d>E9yx$Dh$6qEq5C|-yWhcL^8ZUQi#)be6hIV#q31%9zzWm z73L3$9TzgUn&qk=MM9yhEwp(D1Y!JaL;{?v+0kiR;stqu9`d=MUu$1urx#I^r$GOB zr!0W|0wxfaZ;6~r8b3)Fj5jOfu1Y@KvnV^C=;P@$PB9l-`}o0>l{ZK6GuNyWjzIgw z+C#r;TeP6#GU&zhQkEEy&oZB{rGIUr@#^vOUT`b22QDm`fNx5WscXJ8C(H9#Z1Xdr z!ob7_9o0K8s@OMQ9F3+&`aQ3(~pKJMYuJus$1CkwQY> zv{9YBb=yNQhz)#azGQi_Db~d5`JRB8x*Oa@j$Xvr$*RvWkO^nB z7~=VaWe-Z7>7XHeTFuY&*-{sR$87Ewt9B61Cx3E_x>YJ~!zL1A)Un7V|JY+J%dv9p zM;(zQusq2#<$$fv_{|L$@sJI>xqvyZl9mZ? zC3MG_R#NL5FVM7DZJ?i<;Ij`4+?5s&?J4hK&&pC_b@EY(IbvnRQa+f9HRv*KI>?%LA@`=!$_phN=|tWt`4$-?@^dLW1Zg@U0z zeHFn=k$rV&LK{1Mves|;wPe%%VTx)`|6&(iu-e#w6k&`Iv)^dqmQHnI0y9A^XUhye zNn+}b3Z7VLqZgS%$IPSp>LuWu(ojeHO-}NfC-KUh*{TJXzWd_|2bAp58v7YkW0_b% zH~mH5hN-YiJ<>@_o#eC96hbDT#*%1exUeL})dW3_kLh+BeC{bN#V z2C*$q2t~dQvJry_?1SeyMU~#F=_|QnM?9a3H!847ztMa@8vAVO^n96q`#z-;iLBi= zu0)SiN_U;>Eo$fa zRW*?~t+;}AcwoEym;>f*RofCbN5j!tr_45E$F8xaiRKJ1(`3%@!7BC`kGZvkwHD@R z;KKsrb0P6&QQDkIAa)=FwO>PVQ!%GM8KXLGFx}qqYL=xo>Bk59&KLaO1RUQ#Qh)sZ z(0Kk4^6wvTXBiIM`}&ptYor|+z^`(EU*CO2L15TN>B=msr&g=S8|l{20ZqmsH38}u zT&rjI>XkE~)?JbIp$Dgdlz?0Y1?0CYCphr*DxjMmVJ6o0Iql0>+@Uz&ynzTcnSc?n zSu92i7aq+fW+ci|z05B=7ItM9n-pLA_((CXz&C@E1i%4C&ZhZ;vi3~Bkn~;?(&QHH z-H1%3h(K8pO?QaS(&B$ES`2b98+IEMYi>Kc3Rv7_HtTQBe>vz>!CH0MvLd!&54)So zlu0nOsGFGMk9Gk^433g|j@=wy9BQ{RBkC z5zSp!?f6`yzd~*`wm%;rT*Mm{&q=W(^zs18Y@fjFTZE)}tBr1ZS%r0%-m(Yn=kXl9 zc`P0X)oGqG_{qlArE{2djNGB#d6BH>0Q^wCmS|+0a#Ib|=+;u`)frCboPKsPRJSti z3$esdOr}GUFc}pAQqY$txEYzbkGaMwcB?6~_bsiLNfv_P#N_JuHbcW~ah-#892k@E zWZBW=Nyr5X?L@NNc5^68O5_f76ov@X0Gc5c^ig0o0Crg@r7Z4;`eDMSp=!L2PY*-}!${eD6PoS5 zW|K75IV|67(u5M}O$W`-_)1u|ImSah@Tmhl!)i*NsP~T<9Ot7JGxFL7NxF&7fD|?V zsi)di?tE2k%9!9xkMLm~6|MIQxty1JbOOmr#<$qsy3S);_02vrnqR!9hRlT8U8dg3>559PQ1 zk3LO?!_I60ADrC>u6G*_ScL}CCRL0Yiw~#Rm*>5Sk{0Wbr7e8vl(X}>GhPhtJ}*BP zP}4XvccSjFevU`L5o?om0?<{7hDx5z^76vD=|u(U;!zt-b}nT?hS%n8iteL>bj)5h zu6gz}pCwQcrMQ$_mNA-z<_)-$tVJ5OBAVvwrFgkOx(Ewmy+4OJE~|}2)G`smlcoo< z4)0+MvT~nwwF6nlzCSu>Hfukq6ViL5xUSNvX92xZs?^O+uiFznAY0oGZ!=ub&G8tv zrciRI^PB~#W<Rk8&VEqRLq20Es58?7@Ty*WDjO2@>zMjo7Sx0#2XF}5LIG3r=1MD zu+BSh@&D=CQ3U)SUHdLy0@kzizd457Nyzf;Z|C_RFN3so)z5!1jHn12-N6mN`gFjW z$Z!aKiQ&?2?7eQgy2!v+90S8~`#bG_F}Q05Q%;fZ4!FDFgYNx~^+pAY`w|ZQp=_l| z2pfT8?R2wv?M{U!6M8;4NNgVVT0fQc0EMU*vhwKdsTE1U*A<_%m~05`jlkLa#CAfB zKC>)CYeZ4iq*(F{p+r}tijmSxhOMVo7?|@=5*bZQ?K%kf3PX$c`W-Ce z)AO<*_<9s=om)${ViCo0+k9(tRcPT(bIu1^$Gh2H4Yi{iko#eqr5<5Oz>2jMJl$jW zg;Mz0YJI6ogg7^ZU$>=ilNWtWJdx+6XYDSA=-Amg;&C$)?Np7_btBVfPc=zdOD7lH zCaZ|=4y>OzoPA=VMC1w*B=h(=SQh#86w*7FAlU08Apv^_&I#;BElMBKM*kG%vkEhp2X zOF#V@s77(l0+(Z^!?zm0<`$N8;pF8f4j;7&ST{Ht!?KVaSASO}#dC&i+n0Qv0o3+X z$a)2AggD(EG4iqNOr)ob>rBmE@ms~=MT3hlEn@DIjIpzvZgUu>`YnDVt2D=it%0zC zx$zjIMhSm5$K0;IvY?@q{_C^8>ntu&Yc94$YI>=SrMf3QgpP?m<|y9)Xs#U)e1Ws! zL~~cb+^Schi;x8iP5+|4Vav-H)8lOS?$VFn(@z4XRzhJ@BzK>%wXc&pV5GPCk4NMnZkUOZSa?XfeAh&yZ4S zJm8vCri&P(LkP@F+32gyL)s0*R#9`e1r=l`G3bPN zw)ma>&E|oH!;_iuoBU*Nt;g#or!5cQo%GBBFt<Q+YjErw0zqhwo3SG?*vi=BTVw)MC>v$@dwx=&_>Q$b; zTfFwi=Bu_YEMAINp@yPSD9o}^Uc?$)u@edC_)FJ74Z~F63$)4D$z1QE4B_K4ae-MO z!74_+Zw@8tnO-bdhnI*ObgW_@-d<#mFy=wJ%JG{c8}=lRs4NCEJmlFNd4`ns%8k~5snw!)kxQ|fj=q>7Xd)vvnXkGoBgX<% zQR*YP$iDz>jE;RkNhl(rWE=Qh2jGU4>ei~$5enh>TK`n0U_cTtIYcP>aH|{LC4||IQX_v(ybZ(JEW_5}tB^V!9ADVzwB>1IuawVf$ zl8kbEEFuwgh)KbGKPmSpQ_aN%H#j%H+E37ws_MFZC4ZQhYZ?4O-TaRyB~GCws; z$n^+pm4T93bmJXtu6JEeKD-=0E7=e#G#27`OJw9l2{lK@NM<1+x&7|#O9fX2p=>Hh z-Scp+&Ir&MrW0b%Dw>RyMSU7ExQDvy3Cb!SAZ%`_Te&}vW0A}I%W48po1#n(zrl>9 zz;j5m@u)H->H6N>ZsL``C8Mt1KlTIcg5jE#BpZ!iDulR@(+C^t_>=?yLg{ zm&i$Li7@8Idp-s^_}-NkoD~KM_C-2@D`11J#Wh!1ucFPKIpt7^8LV#8#C%**(m8vl z-uZ$>57T~EoJoCu&dTh%=@@hpC0rNBbhOocs-*cccV#MKRL4Lyr#eruTj^9Hj6}3zX3>s*rZ2A@sP~_N69MJEH(WUR+-)|- zUCEuTW{nDRE|~0U8-Jg1k-_MB4#)opa^aj(dH6M={KT>jOz#}p%BWrwe`fI$cAenP9I(2&IHpy=a%~&HJs62 z=^DTnKMAVSZ#|ZzFdky`8VaLAAv&xogss{5vT#=BHW#S-6v2kQBB7TPIj3&pt+d=f zp_Xgh9-|`$ZBaA?Tub{$MO1mm_7tC-0dcD!;ja2gGFftg?TT=8i7{y-n3z8>=Ym(C z0rK|N1>TA|N^3M24lVrZE0cTd#{yNcTT5gphN_8I!D=1h79di~Hwo@s~zn`)e$ z5>yf+ewi>Jdu3h=TyzpYfr~AQ2XS=C(Uu@vf>Pzo;X8ANmZbpT-7pTSN)1d?T~Q^B zaxZTt23P*YU~i+WXid!OR!)sMrZIwU4pCKRJEo#5<{JogN1Zh;bLRfNU!27ac4_mo zN#pfoojv2omSMhu+PU!z&J^)F`+9KfLh|Rf?Ce4sMTYJuogzOvTvw7-XN75>gKYN z{;=4}!ds(gqg$y(2?EX^77Bl0sj9AE;T*p5jf?QvZ=N`6$z)}`$7WkdhIhJK^m5DFS?NXQ#X zL<2r3`Oll8IZ(-1?2j#qoHo`^l1zvCc+m0Jzi@}_WjB6s$@vh&`H%`7l{ zz&v&im5aQIiSk@)tXmw6fk4J^k#!0dW8AfrUem+kjOBVde9n*4=FXAnXp1gB8ANSz z-?&I!G{K7RbeEWH#K-bnRKJ$k)n^IMQ{@2=@rHvkW#9l}EcSe$jp0ProkiG8ZJeLq z#>em?js)Q%pK%9bj?LbJR+feM18+p&G(&pw4lnz7STj4C|^c>hF zE}NFGR+&oZ5-jY9pd@4)A;jfD?kB+sjEUs~yk0Rreeut_DRLsl4SP5IsHR)*`~pVL5vvB6Mc$$F ziV-HncQSOGCqb|>$COzsKQG&Zw+!@b3jgL=y-Y;K^RjlT;bnbSyfsZE^Mrk4MWDW9 zU-Ce!=fdFBp}7DBXWBu1eN1?QXD-(={!OT>uS1csD=hJd4As%oWFnT}@Z69^PnXnP zJ=0!~Qi1wrVCsCqO1+C+O=1H3iWHj!J@jOj2+q>akJxXm@w-@|S?iZ(Z_4`|d1w-4 zCz6&zN}(iV9ty)#-a4tTJ2v%E&89j0a#>n=_7bNWsK$flEFWhD@3--tkk9X4Vj3)M zbVp#_#)Btp7vYH_XL3*p_682ldRZO@MHZy|>~|irvaccyH9@)j4d8w~yYw;`yv_)t zgCo2ks3$VFH>ljxz!(K5Nace(Rq7Bvo6IC&#>ZldL}sT=wtF+e z3-LT~MpXg;jn$H|DwQZv$igC<=nb~H+RKLk^=*Pt@JkIh9gnLlRZl-~pRyKXD^re5 zXjhp}^2Q&BNf{gOji~p{L4fsHnatuLE%GatKE|wBinh68xi-i95dob&`BE}2&}Lo4 zv*|4!{@#y;z9(^~TygUr#b)kl9wgTQs}OmKQ|@g!a1bco>cs;JGOx;&bUV7;P`!Rc zO|6+yAtZ5x#H9T8Fl4(Ok(ExqP{dIriz2YmvZX{ty+~GQidz(H`%vVgF9~t`lRqVv z2i+(XcEOtzMg^9sPD0~VsL8XKsr&s3e$EYI^DemgNS<}R&d(QfIU{Wr5VA#Jg;@F8hv)?hpTUHHpuX%%wb+| zk2VpC3nRqWYCO3~3KVe9$a#T%km657ob4c}d)>UXHb8@H2AnP3d@|l!_KsYjpWkml zK8HQ8zUGdQ5U})pg{IUMp0H2o4}f=4^SYklwrLP0;&Jtcl+WTWOnm^t9xi!k?Tp1n z!~?CMj@UybDeJ|C9am;f-*yTpvJOlbnDk16`jxS-ieG;4&{`aoQP!la^hCo28GS*h zIFbzV0E~N~y_k|?Wsk&9JcCRwo%<+ae0AM>jOIa@bMLO>i3mnobSXzs!zOg)ErerEm)_|0F~3nz zKbHmRp0`xTJ#KmAXhJDnnXa#M8bk%I&lYXlIe4TE z2Jn z+gtXwu+s?Qg1>#Pv^fx6IyP*|y+yAYQ{303WinuVSrLaOe4djFi7^u?TTcFjoa-)> zO|Ta`ra8A?1A$y7KGzVLyu&rA_O_h)C}d&*L!@bE$(LplkUqrKVA?Q=`43W zx7euaXvI)g@zb)nvcVx|Z?-Z$+NuJk*{idE=En76y{1z~(DM*8tEW(p$R1&z2&B(L z`z&h|em?GyKmJz7QfC^7sb?%nndj4yhejn%lVgMGWN_D9*lBIv3VJng&TlcfdGcwx z<&0HoFA|3l5sF6;N6ncpZDCNk}ffY0XUTb;V&Nl2AqIfXtK>b~^b;bn3?vwCTL!o0UzbdC|>7 zijR!IM$t9K;>mxOo|{IP2_5sNEPU>3y^$bzVL5KUo<{_}LsppSj1}0XMNJO0i5Lru zY>Lk^o@YPWd9914j%OBOmx%^}NK7+?iqE5L-8K?(Y}ENi&l!LFr7|CeaxU8=Q=d(#lv9lG*%ecERR;>@GdI1Ck<>9h5)I+yORlOP z)?n*sbX%J6>jTg(No2FxSwe?~H9oT~JGedlF-iz*9^AsjrYluCAthpx(>q_m$ZOPP zO1K$pSw`Ie1(?0WH!H9S^#%?q&h{$jXP|HaY8(nBO*ARwrbMYdkpQnw!@6PAW z8f9nxWx1;bB35Xw^3D(@MYqe@l(xQ^++)S&l+WhXBv6=F?MC)_p4_g_>M3z{M>ba2 z_v3XONqAdIfJVB`s^PX+V|HYc9QP--`*ZX5B_^jhU5uhTv%?6@4plwV!IQ6txyfA! z=Tx%RRea%5FPit=G|P6Xb`4^AH0G97+%3|P6I}ziiTw8dnYpvJFx?LWF-EIuoKj)o zHJ_kxEr62)+V#-7`Kk1AHs0Gi8N&nTC+3W<~$$N8n#@$Y8lFN`*HA_n@dKU z;u$(>Vx}EuC!xr4Ww7-K2)T^K_O4KUz$7fiAWP49NM*w;U}*e{m9 zT!@+ZK%70B=A^;d-t1+fPlp=St$DWYIb8?IBZG~+U9ryEm~#*v!O3<$sBex(spoU3 z%(BJ6#>qoOXb`Va4wS!LKn5A%>|7hFz^czS7DjN%H_wsd1Uv5`HK*UE!e$bt0Z1=P zX09m5LNfdSAt`X#VBDu*aAuy4D$P*j=##C1%SaeK_vCywX#~Jakv!rT7sM~$&+O%V z2slVBsNvViN;&(SC#me1j!EeTZ}B+(ut))$W&~x!)E+T+Gb4FfWYZ4{BP`cN9;{)O zmrca1HpBIO)m;wsbY+J4acx;{!)kcq1exT;0~^i6Hv%iXm4;0-4%>@+gM|s~thUJa zt{fXT$7Ih_!jcUA*bNS%wfAagN)b4baxcJqvo)pZ+EKHKGMbP!#*s@aSel$+l5 zAyLPtw}0N$g?KxX`!=opd|8{B)Pc;v-rl;0{Y@9J2gBazJBjJ2#BQF~VBcp=`;+Wz z@zmED>SCxg7_9AcqYCf?I~GeyNC@=S0)e0gI~#4o1VKf^;*x=!kCz>6pp=~Mz2Q`E zT8I7h6q2W{>x+pufL4M84?kmE-{BzE=;-MDjFrY&M9&9~I)ygb>n2_G$zP%+_L{wy z2zK;Vyy%mGcUD?s&bL#YHHjEDMnd2q34vPq=5PoaU@HZI2I2>8xarXFs4zuT7zk9%bVz)N z5Q->j_ulv3Hb6MlIo)%%J?_W<_xpd`_wM1{kMvY7-!zf??C}Y;~_+}qB%xn~0u&K%PJ|!0%;_O-Osx1E~qqjZrU|6roO`FIb zm2n|6Qc@E8%~l&`d#a-A;)U{rEa_0RgDAegzZoIjxO3xUM9P%MZu{|NM95(Jh_d@B1hAYuxLanDxq*XMcu@&(7(&^lMAPO;y#? zZ1c9MpBvizq2li7cWSS2uZSxuxkTB{`jkOY1owE0avxYO8}?cvZkFz>>}`3u1>w5_tG$TpBtB5Zln*K?ORbNm@Q53oD%JQpvnDoX6rriYSvNV1uM3Fr#Ry75uRnLL-WGF z#M~mEHNMZBBSoqx=T6PjlUto;@O`ol2GEtXM$X~N`8h>K59Y+&znaL;^Sb_f@w9S5 zMN^%pu}>tcWLpq9Q_^vQ9=O~(0i5iSqpO3rk?c-T*fclmNAK)kkZ+@KA4q!)HYi_DE}&4Q*yWWzFoHk zXTjt~)yfs-_CJ^P)v9x4U!HTznU}68lw9+1+3W(&leLl266Lq6B~}%iE{BzN+`8oY zu)D@=lj8@u*9-U_hwB@E8y%BY!;FZB(6rEn>Xnx;``a`yX61~RLe*Flp z;AetFTA7Z&3H(N$p$Zyo>Zh2N8j!)dBb2}!K`Wy5+JZ#JO4ZiMCk>xEzTwkS%9Ct6 zrrxzInWemPhC$`}8uR*@4+H${ejI3*?b}N^_Pp*ByeZo6eb&s|rN(y27I{Y#c~D&(MAQYp!71UP?3qsPE^&bZ zmTQ0mv+CIM?1fW8Q5E#Y7A$-v&5~+D)>b#K2@3ST^ManscKh&=@)1Q(%fZiy9f5amhm)7S)gs0L@S6yk0Bk}1^-0y4LdqF?w51AE2{1* zcmCBDoRsXC^({&GS=r)WER+lG4V&yJ2z9PL*fd?ePctp{bwy85T)3&^P>!tr}+JvPLI$96f*lEUN2+9BefcNAa(?lS3M?PZjHECIz8>H-q z*i%V`bkSKz*%r|k?gGTd@&;N%5d|?oYd{QW4Tx3x&+(7odh26bTVQ%(^@j^$-bq6$ z_#thmy9?>aK1sHq5u;yt5g19&3b>KT z6LXD1=b-Vegjypj0XJAEQER9oB)(6hB@V0vWe~(2arbGxi@6WC66QWUHpYk6IS4@` z2kAjW2cek4p`nA2W3j-x>;=@Gkod^f4I>-fv)C z;-9u2FX;cBJ5P@4GOzWBxi%27zHSYeqyF#@f<<>LAQS_^^7<&pg~KS<_2z#Yr!7RE z!KFI^YxrLjFiYfGuMHicZit zSD4m#;kfgT31p)K@Oi1904T19p@+#XVtfOzbcS$u@`z%!4S&W8aiZ%MjufDcMr$|#vpqbkVElPM@cKSijVXVTxU+gMo6OwBf%s? s4-zpB8Kp2}>VYav`~{)|OXKRJ4Vw3v+UUlbOeL_PLMAY8`m5%@0W8lye*gdg literal 0 HcmV?d00001 diff --git a/demo/ai-todo-assistant/diagrams/aifa-frontend-ui-slots.svg b/demo/ai-todo-assistant/diagrams/aifa-frontend-ui-slots.svg new file mode 100644 index 0000000..e10dd71 --- /dev/null +++ b/demo/ai-todo-assistant/diagrams/aifa-frontend-ui-slots.svg @@ -0,0 +1,92 @@ + + AIFA frontend UI slot composition + Feature-owned UI contributions are discovered, typed, and composed into named slots published by an application shell. + + + + + + + + FRONTEND EXTENSION WITH UI SLOTS + The shell owns the where. Features own the what. + Named, typed extension points keep layout stable while feature UI evolves independently. + + FEATURE-OWNED CONTRIBUTIONS + + + Generate Task Plan + contributes → AssistantPanel + model → PlannerSlotModel + + + + Create Task + contributes → TaskComposer + model → ComposerSlotModel + + + + Task Row Actions + contributes → TaskRowActions + model → RowActionsSlotModel + + + + + GENERIC COMPOSITION + + 1 + Discoverfeature-local exports + + + 2 + Type-checkslot ↔ model contract + + + 3 + Order + renderdeterministic registry + + + + + + + + APP SHELL + layout + navigation + auth + + + APPHEADER SLOT + + identity UI + + + ASSISTANTPANEL SLOT + + task-plan-review + + + APPCONTENT SLOT + + task board + + task row + + RowActions slot + + + Why UI slots make extension easy + No feature imports in the shell · small typed models · deterministic composition · independent UI testing · add or remove contributions without layout rewrites + diff --git a/demo/ai-todo-assistant/diagrams/aifa-system-overview.png b/demo/ai-todo-assistant/diagrams/aifa-system-overview.png new file mode 100644 index 0000000000000000000000000000000000000000..310567344de84f30ec6956ad4d3e30708eaf08d8 GIT binary patch literal 110459 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 z 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

      QY~Z&=>m|R(0LOm8IXQV3$-MS1 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 literal 0 HcmV?d00001 diff --git a/demo/ai-todo-assistant/diagrams/aifa-system-overview.svg b/demo/ai-todo-assistant/diagrams/aifa-system-overview.svg new file mode 100644 index 0000000..5349b72 --- /dev/null +++ b/demo/ai-todo-assistant/diagrams/aifa-system-overview.svg @@ -0,0 +1,109 @@ + + AIFA system overview + A central feature contract connects governed backend adapters and capabilities with frontend feature contributions and typed UI slots. + + + + + + + + + + + + + + + + + + + + + + + + AI ATOMIC FEATURE ARCHITECTURE + One feature. Two extension paths. + The feature contract is the stable center; transport, infrastructure, and shell layout remain replaceable. + + + + + + Backend extension path + Frontend extension path + + + HTTP / MCP + Transport adapts + untrusted requests + into one contract + + + + + AIFA runtime + Validates actor, + metadata, contract, + and capabilities + + + + + Bound + capabilities + Data · AI + Audit · Events + + + Feature UI + Own view, state, + interaction, and + feature commands + + + + + Slot registry + Discovers, types, + orders, and renders + contributions + + + + + App shell + Layout + Navigation + Composition + + + FEATURE CONTRACT + + + + + + Same governed operation everywhere + Authorization · tenancy · validation · idempotency · audit · events + + + Open for extension, stable in composition + New feature UI plugs into typed slots without rewriting the shell + + Small features · explicit contracts · safe composition + Local reasoning for humans and AI agents, with platform-wide guardrails. + diff --git a/demo/ai-todo-assistant/docker-compose.yml b/demo/ai-todo-assistant/docker-compose.yml new file mode 100644 index 0000000..277626f --- /dev/null +++ b/demo/ai-todo-assistant/docker-compose.yml @@ -0,0 +1,30 @@ +services: + mongodb: + image: mongo:8 + command: ["mongod", "--replSet", "rs0", "--bind_ip_all"] + ports: + - "27017:27017" + volumes: + - mongo-data:/data/db + mongo-init: + image: mongo:8 + depends_on: + - mongodb + restart: "no" + command: + [ + "mongosh", + "--host", + "mongodb:27017", + "--eval", + "rs.initiate({_id: 'rs0', members: [{_id: 0, host: 'mongodb:27017'}]})", + ] + ollama: + image: ollama/ollama:latest + ports: + - "11434:11434" + volumes: + - ollama-data:/root/.ollama +volumes: + mongo-data: + ollama-data: diff --git a/demo/ai-todo-assistant/index.html b/demo/ai-todo-assistant/index.html new file mode 100644 index 0000000..f5dacf8 --- /dev/null +++ b/demo/ai-todo-assistant/index.html @@ -0,0 +1,16 @@ + + + + + + + + + Focusly — AI-assisted focus workspace + + + +

      + + + diff --git a/demo/ai-todo-assistant/package-lock.json b/demo/ai-todo-assistant/package-lock.json index dc12852..fc7c94e 100644 --- a/demo/ai-todo-assistant/package-lock.json +++ b/demo/ai-todo-assistant/package-lock.json @@ -7,12 +7,30 @@ "": { "name": "ai-todo-assistant", "version": "0.1.0", + "dependencies": { + "@emotion/react": "^11.14.0", + "@emotion/styled": "^11.14.0", + "@modelcontextprotocol/sdk": "^1.12.0", + "@mui/icons-material": "^7.0.2", + "@mui/material": "^7.0.2", + "dotenv": "^16.4.7", + "jose": "^6.2.9", + "mongodb": "^6.12.0", + "react": "^19.0.0", + "react-dom": "^19.0.0", + "vite": "^6.0.7", + "zod": "^4.4.3" + }, "devDependencies": { "@types/node": "^22.10.2", + "@types/react": "^19.2.18", + "@types/react-dom": "^19.2.4", "ajv": "^8.17.1", "ajv-formats": "^3.0.1", + "concurrently": "^9.1.2", "dependency-cruiser": "^16.10.0", "json-schema-to-typescript": "^15.0.4", + "mongodb-memory-server": "^11.2.0", "tsx": "^4.19.2", "typescript": "^5.7.2" }, @@ -38,6 +56,291 @@ "url": "https://github.com/sponsors/philsturgeon" } }, + "node_modules/@babel/code-frame": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz", + "integrity": "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==", + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.29.7", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/generator": { + "version": "7.29.8", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.8.tgz", + "integrity": "sha512-gZbepsdh3WDtgZKWL+vTPh71LSBrm/Y4/QDZBVCcYfmeTEEuoOYwlSy+G1StfJg+/Zy550u/3TATbm7qDbbMtg==", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.8", + "@babel/types": "^7.29.8", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.29.7.tgz", + "integrity": "sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.29.7.tgz", + "integrity": "sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g==", + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz", + "integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz", + "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.8", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.8.tgz", + "integrity": "sha512-E8lTAYNB1KW+FH+VGJuZM1ioAx2E6oVlvQFRrf5P8ZZmsiJXYAD9vTFV7yyEURNzgh1dFqMZuO6tUwcARbqFCA==", + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.8" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/runtime": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.7.tgz", + "integrity": "sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/template": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.29.7.tgz", + "integrity": "sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==", + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.29.8", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.8.tgz", + "integrity": "sha512-I5z7H3bf/41ktsNVLtpN0wAa336HkqIHQ5BuPLEhTkt1jVSyZpeNKIzTgEWmlxjdg81R0IgUCcaE+Ok3NvrfZg==", + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/generator": "^7.29.8", + "@babel/helper-globals": "^7.29.7", + "@babel/parser": "^7.29.8", + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.8", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.8", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.8.tgz", + "integrity": "sha512-Vj1jF3cPfxg7OAfoI7QnVKLoILlm2JF9pnVHrX8qx7AHMiYWT+NDAA7jChlNgRS4WTLc/fD1lXLmPixluj+3Gg==", + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@emotion/babel-plugin": { + "version": "11.13.5", + "resolved": "https://registry.npmjs.org/@emotion/babel-plugin/-/babel-plugin-11.13.5.tgz", + "integrity": "sha512-pxHCpT2ex+0q+HH91/zsdHkw/lXd468DIN2zvfvLtPKLLMo6gQj7oLObq8PhkrxOZb/gGCq03S3Z7PDhS8pduQ==", + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.16.7", + "@babel/runtime": "^7.18.3", + "@emotion/hash": "^0.9.2", + "@emotion/memoize": "^0.9.0", + "@emotion/serialize": "^1.3.3", + "babel-plugin-macros": "^3.1.0", + "convert-source-map": "^1.5.0", + "escape-string-regexp": "^4.0.0", + "find-root": "^1.1.0", + "source-map": "^0.5.7", + "stylis": "4.2.0" + } + }, + "node_modules/@emotion/cache": { + "version": "11.14.0", + "resolved": "https://registry.npmjs.org/@emotion/cache/-/cache-11.14.0.tgz", + "integrity": "sha512-L/B1lc/TViYk4DcpGxtAVbx0ZyiKM5ktoIyafGkH6zg/tj+mA+NE//aPYKG0k8kCHSHVJrpLpcAlOBEXQ3SavA==", + "license": "MIT", + "dependencies": { + "@emotion/memoize": "^0.9.0", + "@emotion/sheet": "^1.4.0", + "@emotion/utils": "^1.4.2", + "@emotion/weak-memoize": "^0.4.0", + "stylis": "4.2.0" + } + }, + "node_modules/@emotion/hash": { + "version": "0.9.2", + "resolved": "https://registry.npmjs.org/@emotion/hash/-/hash-0.9.2.tgz", + "integrity": "sha512-MyqliTZGuOm3+5ZRSaaBGP3USLw6+EGykkwZns2EPC5g8jJ4z9OrdZY9apkl3+UP9+sdz76YYkwCKP5gh8iY3g==", + "license": "MIT" + }, + "node_modules/@emotion/is-prop-valid": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@emotion/is-prop-valid/-/is-prop-valid-1.4.0.tgz", + "integrity": "sha512-QgD4fyscGcbbKwJmqNvUMSE02OsHUa+lAWKdEUIJKgqe5IwRSKd7+KhibEWdaKwgjLj0DRSHA9biAIqGBk05lw==", + "license": "MIT", + "dependencies": { + "@emotion/memoize": "^0.9.0" + } + }, + "node_modules/@emotion/memoize": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.9.0.tgz", + "integrity": "sha512-30FAj7/EoJ5mwVPOWhAyCX+FPfMDrVecJAM+Iw9NRoSl4BBAQeqj4cApHHUXOVvIPgLVDsCFoz/hGD+5QQD1GQ==", + "license": "MIT" + }, + "node_modules/@emotion/react": { + "version": "11.14.0", + "resolved": "https://registry.npmjs.org/@emotion/react/-/react-11.14.0.tgz", + "integrity": "sha512-O000MLDBDdk/EohJPFUqvnp4qnHeYkVP5B0xEG0D/L7cOKP9kefu2DXn8dj74cQfsEzUqh+sr1RzFqiL1o+PpA==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.18.3", + "@emotion/babel-plugin": "^11.13.5", + "@emotion/cache": "^11.14.0", + "@emotion/serialize": "^1.3.3", + "@emotion/use-insertion-effect-with-fallbacks": "^1.2.0", + "@emotion/utils": "^1.4.2", + "@emotion/weak-memoize": "^0.4.0", + "hoist-non-react-statics": "^3.3.1" + }, + "peerDependencies": { + "react": ">=16.8.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@emotion/serialize": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@emotion/serialize/-/serialize-1.3.3.tgz", + "integrity": "sha512-EISGqt7sSNWHGI76hC7x1CksiXPahbxEOrC5RjmFRJTqLyEK9/9hZvBbiYn70dw4wuwMKiEMCUlR6ZXTSWQqxA==", + "license": "MIT", + "dependencies": { + "@emotion/hash": "^0.9.2", + "@emotion/memoize": "^0.9.0", + "@emotion/unitless": "^0.10.0", + "@emotion/utils": "^1.4.2", + "csstype": "^3.0.2" + } + }, + "node_modules/@emotion/sheet": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@emotion/sheet/-/sheet-1.4.0.tgz", + "integrity": "sha512-fTBW9/8r2w3dXWYM4HCB1Rdp8NLibOw2+XELH5m5+AkWiL/KqYX6dc0kKYlaYyKjrQ6ds33MCdMPEwgs2z1rqg==", + "license": "MIT" + }, + "node_modules/@emotion/styled": { + "version": "11.14.1", + "resolved": "https://registry.npmjs.org/@emotion/styled/-/styled-11.14.1.tgz", + "integrity": "sha512-qEEJt42DuToa3gurlH4Qqc1kVpNq8wO8cJtDzU46TjlzWjDlsVyevtYCRijVq3SrHsROS+gVQ8Fnea108GnKzw==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.18.3", + "@emotion/babel-plugin": "^11.13.5", + "@emotion/is-prop-valid": "^1.3.0", + "@emotion/serialize": "^1.3.3", + "@emotion/use-insertion-effect-with-fallbacks": "^1.2.0", + "@emotion/utils": "^1.4.2" + }, + "peerDependencies": { + "@emotion/react": "^11.0.0-rc.0", + "react": ">=16.8.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@emotion/unitless": { + "version": "0.10.0", + "resolved": "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.10.0.tgz", + "integrity": "sha512-dFoMUuQA20zvtVTuxZww6OHoJYgrzfKM1t52mVySDJnMSEa08ruEvdYQbhvyu6soU+NeLVd3yKfTfT0NeV6qGg==", + "license": "MIT" + }, + "node_modules/@emotion/use-insertion-effect-with-fallbacks": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@emotion/use-insertion-effect-with-fallbacks/-/use-insertion-effect-with-fallbacks-1.2.0.tgz", + "integrity": "sha512-yJMtVdH59sxi/aVJBpk9FQq+OR8ll5GT8oWd57UpeaKEVGab41JWaCFA7FRLoMLloOZF/c/wsPoe+bfGmRKgDg==", + "license": "MIT", + "peerDependencies": { + "react": ">=16.8.0" + } + }, + "node_modules/@emotion/utils": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/@emotion/utils/-/utils-1.4.2.tgz", + "integrity": "sha512-3vLclRofFziIa3J2wDh9jjbkUz9qk5Vi3IZ/FSTKViB0k+ef0fPV7dYrUIugbgupYDx7v9ud/SjrtEP8Y4xLoA==", + "license": "MIT" + }, + "node_modules/@emotion/weak-memoize": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@emotion/weak-memoize/-/weak-memoize-0.4.0.tgz", + "integrity": "sha512-snKqtPW01tN0ui7yu9rGv69aJXr/a/Ywvl11sUjNtEcRc+ng/mQriFL0wLXMef74iHa/EkftbDzU9F8iFbH+zg==", + "license": "MIT" + }, "node_modules/@esbuild/aix-ppc64": { "version": "0.28.2", "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.2.tgz", @@ -480,939 +783,4532 @@ "node": ">=18" } }, - "node_modules/@jsdevtools/ono": { - "version": "7.1.3", - "resolved": "https://registry.npmjs.org/@jsdevtools/ono/-/ono-7.1.3.tgz", - "integrity": "sha512-4JQNk+3mVzK3xh2rqd6RB4J46qUR19azEHBneZyTZM+c456qOrbbM/5xcR8huNCCcbVt7+UmizG6GuUvPvKUYg==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/json-schema": { - "version": "7.0.15", - "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", - "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/lodash": { - "version": "4.17.25", - "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.25.tgz", - "integrity": "sha512-+K1NIO8I+F9/wNulfVvu23QYd0Pe9/OCqRrim4NoYIf1VoEDL90Ve4ClzpyqBLc7NpGGWRvYNCKZ1BE/Jpf8dQ==", - "dev": true, - "license": "MIT" + "node_modules/@hono/node-server": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-2.1.1.tgz", + "integrity": "sha512-ELuehkj5VCBdgEw9zs+ivkKwyzzUCSQuE96YmiPvn1ECBoZCczbFXJLeEGMTYjphP6gydh4pHMqEYPVMYUVgQg==", + "license": "MIT", + "engines": { + "node": ">=20" + }, + "peerDependencies": { + "hono": "^4" + } }, - "node_modules/@types/node": { - "version": "22.20.1", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.20.1.tgz", - "integrity": "sha512-EANqOCF9QFyra+4pfxUcX9STKJpCLjMbObVzljIJomAWSnuSIEAvyzEU53GaajbXJEgdh0iEcPL+DGvpUd4k1Q==", - "dev": true, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", "license": "MIT", "dependencies": { - "undici-types": "~6.21.0" + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" } }, - "node_modules/acorn": { - "version": "8.18.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.18.0.tgz", - "integrity": "sha512-lGq+9yr1/GuAWaVYIHRjvvySG5/4VfKIvC8EWxStPdcDh/Ka7FG3twP6v4d5BkravUilhIAsG4Qj83t02LWUPQ==", - "dev": true, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", "license": "MIT", - "bin": { - "acorn": "bin/acorn" - }, "engines": { - "node": ">=0.4.0" + "node": ">=6.0.0" } }, - "node_modules/acorn-jsx": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", - "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", - "dev": true, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", "license": "MIT", - "peerDependencies": { - "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" } }, - "node_modules/acorn-jsx-walk": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/acorn-jsx-walk/-/acorn-jsx-walk-2.0.0.tgz", - "integrity": "sha512-uuo6iJj4D4ygkdzd6jPtcxs8vZgDX9YFIkqczGImoypX2fQ4dVImmu3UzA4ynixCIMTrEOWW+95M2HuBaCEOVA==", + "node_modules/@jsdevtools/ono": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/@jsdevtools/ono/-/ono-7.1.3.tgz", + "integrity": "sha512-4JQNk+3mVzK3xh2rqd6RB4J46qUR19azEHBneZyTZM+c456qOrbbM/5xcR8huNCCcbVt7+UmizG6GuUvPvKUYg==", "dev": true, "license": "MIT" }, - "node_modules/acorn-loose": { - "version": "8.5.2", - "resolved": "https://registry.npmjs.org/acorn-loose/-/acorn-loose-8.5.2.tgz", - "integrity": "sha512-PPvV6g8UGMGgjrMu+n/f9E/tCSkNQ2Y97eFvuVdJfG11+xdIeDcLyNdC8SHcrHbRqkfwLASdplyR6B6sKM1U4A==", - "dev": true, + "node_modules/@modelcontextprotocol/sdk": { + "version": "1.30.0", + "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.30.0.tgz", + "integrity": "sha512-xKd8OIzlqNzcqcNumGAa6g+PW2kjD5vrpcKOnfldAUPP3j7lnqMPwlTXQm8gF+UwH72z0lqaRbjr9hqGz0eITA==", "license": "MIT", "dependencies": { - "acorn": "^8.15.0" + "@hono/node-server": "^1.19.9 || ^2.0.5", + "ajv": "^8.17.1", + "ajv-formats": "^3.0.1", + "content-type": "^1.0.5", + "cors": "^2.8.5", + "cross-spawn": "^7.0.5", + "eventsource": "^3.0.2", + "eventsource-parser": "^3.0.0", + "express": "^5.2.1", + "express-rate-limit": "^8.2.1", + "hono": "^4.11.4", + "jose": "^6.1.3", + "json-schema-typed": "^8.0.2", + "pkce-challenge": "^5.0.0", + "raw-body": "^3.0.0", + "zod": "^3.25 || ^4.0", + "zod-to-json-schema": "^3.25.1" }, "engines": { - "node": ">=0.4.0" + "node": ">=18" + }, + "peerDependencies": { + "@cfworker/json-schema": "^4.1.1", + "zod": "^3.25 || ^4.0" + }, + "peerDependenciesMeta": { + "@cfworker/json-schema": { + "optional": true + }, + "zod": { + "optional": false + } } }, - "node_modules/acorn-walk": { - "version": "8.3.5", - "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.5.tgz", - "integrity": "sha512-HEHNfbars9v4pgpW6SO1KSPkfoS0xVOM/9UzkJltjlsHZmJasxg8aXkuZa7SMf8vKGIBhpUsPluQSqhJFCqebw==", - "dev": true, + "node_modules/@mongodb-js/saslprep": { + "version": "1.4.13", + "resolved": "https://registry.npmjs.org/@mongodb-js/saslprep/-/saslprep-1.4.13.tgz", + "integrity": "sha512-E3Sv4eCYAlKYUTx8S3ioQcDUscOif+8zZ5OnW1IzJ+Tt+EO+ke8mn+Y3FX6N1H79picwbdOavVOb1jPi2EOyrg==", "license": "MIT", "dependencies": { - "acorn": "^8.11.0" - }, - "engines": { - "node": ">=0.4.0" + "sparse-bitfield": "^3.0.3" } }, - "node_modules/ajv": { - "version": "8.20.0", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz", - "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==", - "dev": true, + "node_modules/@mui/core-downloads-tracker": { + "version": "7.3.11", + "resolved": "https://registry.npmjs.org/@mui/core-downloads-tracker/-/core-downloads-tracker-7.3.11.tgz", + "integrity": "sha512-a7I/b/nBTdXYz2cOSlEmkQ9WWE1x8FHpqMhFPp+Y1VPFxcOw91G5ELOHARQAGSPy5V+UCgJua6K/1x70bAtQPw==", "license": "MIT", - "dependencies": { - "fast-deep-equal": "^3.1.3", - "fast-uri": "^3.0.1", - "json-schema-traverse": "^1.0.0", - "require-from-string": "^2.0.2" - }, "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" + "type": "opencollective", + "url": "https://opencollective.com/mui-org" } }, - "node_modules/ajv-formats": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-3.0.1.tgz", - "integrity": "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==", - "dev": true, + "node_modules/@mui/icons-material": { + "version": "7.3.11", + "resolved": "https://registry.npmjs.org/@mui/icons-material/-/icons-material-7.3.11.tgz", + "integrity": "sha512-+hz5ilwHZ3djd5es3sCErLioqe/NhZcYTsV/TNXZAMdJdb23F4xzJjqnnZdnurc3S1+ietcssRNqieOhPQLZ7Q==", "license": "MIT", "dependencies": { - "ajv": "^8.0.0" + "@babel/runtime": "^7.28.6" + }, + "engines": { + "node": ">=14.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" }, "peerDependencies": { - "ajv": "^8.0.0" + "@mui/material": "^7.3.11", + "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0", + "react": "^17.0.0 || ^18.0.0 || ^19.0.0" }, "peerDependenciesMeta": { - "ajv": { + "@types/react": { "optional": true } } }, - "node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, + "node_modules/@mui/material": { + "version": "7.3.11", + "resolved": "https://registry.npmjs.org/@mui/material/-/material-7.3.11.tgz", + "integrity": "sha512-yq8bPc3LxOwKRWpcjRgDkYFmpM6aKlARfESTmOQcvLYFeJwtHte2tw6hJDrb8sk8wcvpDprHEHVaoUU0MslIkw==", "license": "MIT", "dependencies": { - "color-convert": "^2.0.1" + "@babel/runtime": "^7.28.6", + "@mui/core-downloads-tracker": "^7.3.11", + "@mui/system": "^7.3.11", + "@mui/types": "^7.4.12", + "@mui/utils": "^7.3.11", + "@popperjs/core": "^2.11.8", + "@types/react-transition-group": "^4.4.12", + "clsx": "^2.1.1", + "csstype": "^3.2.3", + "prop-types": "^15.8.1", + "react-is": "^19.2.3", + "react-transition-group": "^4.4.5" }, "engines": { - "node": ">=8" + "node": ">=14.0.0" }, "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + }, + "peerDependencies": { + "@emotion/react": "^11.5.0", + "@emotion/styled": "^11.3.0", + "@mui/material-pigment-css": "^7.3.11", + "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0", + "react": "^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@emotion/react": { + "optional": true + }, + "@emotion/styled": { + "optional": true + }, + "@mui/material-pigment-css": { + "optional": true + }, + "@types/react": { + "optional": true + } } }, - "node_modules/argparse": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", - "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", - "dev": true, - "license": "Python-2.0" - }, - "node_modules/chalk": { + "node_modules/@mui/private-theming": { + "version": "7.3.11", + "resolved": "https://registry.npmjs.org/@mui/private-theming/-/private-theming-7.3.11.tgz", + "integrity": "sha512-9B+YKms0fRHbNrqp9tOT/DNbNnU5gyvJ1o3qAGXfq8GmZcbJnE3At9x07Zr/o0pkhzg4aDdwXVqe4+AcgtOCPA==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.28.6", + "@mui/utils": "^7.3.11", + "prop-types": "^15.8.1" + }, + "engines": { + "node": ">=14.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + }, + "peerDependencies": { + "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0", + "react": "^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@mui/styled-engine": { + "version": "7.3.10", + "resolved": "https://registry.npmjs.org/@mui/styled-engine/-/styled-engine-7.3.10.tgz", + "integrity": "sha512-WxE9SiF8xskAQqGjsp0poXCkCqsoXFEsSr0HBXfApmGHR+DBnXRp+z46Vsltg4gpPM4Z96DeAQRpeAOnhNg7Ng==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.28.6", + "@emotion/cache": "^11.14.0", + "@emotion/serialize": "^1.3.3", + "@emotion/sheet": "^1.4.0", + "csstype": "^3.2.3", + "prop-types": "^15.8.1" + }, + "engines": { + "node": ">=14.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + }, + "peerDependencies": { + "@emotion/react": "^11.4.1", + "@emotion/styled": "^11.3.0", + "react": "^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@emotion/react": { + "optional": true + }, + "@emotion/styled": { + "optional": true + } + } + }, + "node_modules/@mui/system": { + "version": "7.3.11", + "resolved": "https://registry.npmjs.org/@mui/system/-/system-7.3.11.tgz", + "integrity": "sha512-7izwGWdNawAKpBKcRlx7f2gFnAAjmASBWvMcyX4YYEeLOFsbfGRbUYGInvnAcUeql3rPxI7F9Ft4oY2OLRz44g==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.28.6", + "@mui/private-theming": "^7.3.11", + "@mui/styled-engine": "^7.3.10", + "@mui/types": "^7.4.12", + "@mui/utils": "^7.3.11", + "clsx": "^2.1.1", + "csstype": "^3.2.3", + "prop-types": "^15.8.1" + }, + "engines": { + "node": ">=14.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + }, + "peerDependencies": { + "@emotion/react": "^11.5.0", + "@emotion/styled": "^11.3.0", + "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0", + "react": "^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@emotion/react": { + "optional": true + }, + "@emotion/styled": { + "optional": true + }, + "@types/react": { + "optional": true + } + } + }, + "node_modules/@mui/types": { + "version": "7.4.12", + "resolved": "https://registry.npmjs.org/@mui/types/-/types-7.4.12.tgz", + "integrity": "sha512-iKNAF2u9PzSIj40CjvKJWxFXJo122jXVdrmdh0hMYd+FR+NuJMkr/L88XwWLCRiJ5P1j+uyac25+Kp6YC4hu6w==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.28.6" + }, + "peerDependencies": { + "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@mui/utils": { + "version": "7.3.11", + "resolved": "https://registry.npmjs.org/@mui/utils/-/utils-7.3.11.tgz", + "integrity": "sha512-XTjGnifwteg71/ij+0e7Y7d+hwyntMYP5wPoA/g2drdGH+Flkvjwy0OfrVpKBbaOvofq4zU/LIyUZyKgmWu18g==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.28.6", + "@mui/types": "^7.4.12", + "@types/prop-types": "^15.7.15", + "clsx": "^2.1.1", + "prop-types": "^15.8.1", + "react-is": "^19.2.3" + }, + "engines": { + "node": ">=14.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + }, + "peerDependencies": { + "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0", + "react": "^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@napi-rs/lzma-linux-x64-gnu": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/@napi-rs/lzma-linux-x64-gnu/-/lzma-linux-x64-gnu-1.5.1.tgz", + "integrity": "sha512-oTXEIha4SsuXdTA4Iyskj0kpdx2yVXdhd75c2v3xGrHFfVMsbhTPZU/nMPL4sWKo4pBHm3aucLaqGlF696dTyQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^22.20 || ^24.12 || >=25" + } + }, + "node_modules/@popperjs/core": { + "version": "2.11.8", + "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz", + "integrity": "sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/popperjs" + } + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.62.4.tgz", + "integrity": "sha512-RrPokAb7dmbxFoeO3TloqHyOjgye8RkBhSqmp4aJMIex4c9r46ZstPnleDQOq1t46VOVjwIuwNogIqbodV1Vvg==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.62.4.tgz", + "integrity": "sha512-JKuJc+pnpks2pjy7L/N3v/cAkZxYlnmuZoD840ldbMI5KDbC4iO9NKwPKYdjYFCMAIIlBzYSFHxIJVYzRo2/8A==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.62.4.tgz", + "integrity": "sha512-krw5uS2STmvJ02x0uTXHbqQNuz+9eZ1iw+qXk9dmW2gvV4jV7O2hEoOnuhFrpOPiel1mBFtqbxYZZtC46hXLOw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.62.4.tgz", + "integrity": "sha512-wsTxtgApb4PrOsNJIm0FZ1h3WvCC+k9uxLJ4ad75hgoS4NiRes2SoJFlDAyMwiUY8IssDqGcHbXuN0sx1tfF1A==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.62.4.tgz", + "integrity": "sha512-GUOnQlyZe3yAXhWOtOMsn5Qkrv5E5mZXa0thbARWi5Ei2szlVXJFQhddZ4HbAzh8q92w5twp+CQvs/eFanz9YQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.62.4.tgz", + "integrity": "sha512-/Y7f3QuxjzPKsjA/rfEDa3+0vXqyjmJ50Ln8dPpCmWkKTrUoWHG1cWhTqaAMLob2m2nESWuC7yGrREz019Ztqg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.62.4.tgz", + "integrity": "sha512-81wiiX3v7aqy+T+bT61TJ78yJjRquqFFTTbAPt08imfQQzkPIW8t6aJbkTagtCCrXMNc9D66+geqlK7ydLPNqA==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.62.4.tgz", + "integrity": "sha512-9kmDIvNZqdoHOBZgNtpTBeLWYO/LVipM3H/j62P8848/l/VPEQL6N3uxU9pvP1oZAsXyC2MEnFP3ovRjo7WYNQ==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.62.4.tgz", + "integrity": "sha512-CcnXHWnXg69g+DX5VWL3FHts3qMRN2uVEHX+BZvGLdd07/gXkn3ePjYtO1LDJvxkGKVHMclKBRa1QUTH+6toYQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.62.4.tgz", + "integrity": "sha512-iFOibiHnTRuhrWLlRsOQFdZJJIa7S8OwkneJr4ocALP16u5yk6lWLINFwhHaEqBFMsKDUZofLkGos7+CPzGB3g==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.62.4.tgz", + "integrity": "sha512-XnWYMI7euHlb5a871xPja+Gm7DRCFU+FGRrtS2sMq9N8FvqtpagUy6gD4YOemC5MRk9xbh8+jYMEJbigFQwsgA==", + "cpu": [ + "loong64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.62.4.tgz", + "integrity": "sha512-qGDAlO0U8xedCcsdRm9oaoQY8DAx/QT7uIxJWhCdx0ceIWX783UC9QSYkdpzAe29wNiVfp24+bZdQmn49o45SQ==", + "cpu": [ + "loong64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.62.4.tgz", + "integrity": "sha512-ru4H6ezD7ysA5EiEK6qkkaEb4modH8CTej6kUy/gQi20u3kB3G7Zn8snXXkeJSCOFKG/rbPPtM/+9Wgas1961w==", + "cpu": [ + "ppc64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.62.4.tgz", + "integrity": "sha512-2W4MO5WQVJnbJaZdvDb9rhBDuFU1nKIepPFpJUBsTh2k1YY2g+ODViaWuyOAjQ5cOP7NvrvLzt3wvHOoiAvc7w==", + "cpu": [ + "ppc64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.62.4.tgz", + "integrity": "sha512-+fxjfuoAmVMCYV5QyjoIpu0cp5DOiOTeqYFk1AVaxGr+/ravWLX89XfQmptsoWcaVy/TGf2hexzbUOrCQIL1CQ==", + "cpu": [ + "riscv64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.62.4.tgz", + "integrity": "sha512-jTn8JfHGL4djjFxPuM06LmNUJDsst2jeVlsd9OmIH6zc5sC9K6rIuO4YajXatLUpBmBKl6b35ro1QZocLi+tcA==", + "cpu": [ + "riscv64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.62.4.tgz", + "integrity": "sha512-oCJCJL4pXsoDcP2QZ+JVlPTIRc6266zsIaeJJsWImmF7HO0W8nb6HuSgZlMWxJwaPf8ehbSw8yo0EUw925hKsA==", + "cpu": [ + "s390x" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.62.4.tgz", + "integrity": "sha512-W69hukhZ3KKNRCaMIEzKvcFye42hh0FE1+YoYaf5+Ikacuftoco6yO/xouz0hc5d5W/s3yBro5jRiuEE/Q5vUw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.62.4.tgz", + "integrity": "sha512-qiXbGG2jkjXhzXpsFZSR2Xpb8DN/UaxYsbb/STbuR/6fpaDgRmmaq1B/LmtF2wQFOFOSsK2jdE0RZ3a0zHn4QA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.62.4.tgz", + "integrity": "sha512-nWeM//hxv8mIo6jD7Hu4o48DVmV9pbV6gsKaWU+4NFyqHoPKwrkRiZGLKUhOBk8qNmDmpwFtPKg80Bo/Tn4xiQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.62.4.tgz", + "integrity": "sha512-s62SQ/vgsRSvMwDkOEfTqfgASF0f26ZNaQuTA6Aok5lrikf89yI2W0gFHvZb2Jpgc6N8JnOKZgCK2iciO3CsxQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.62.4.tgz", + "integrity": "sha512-J6wGf8TVGbXJq+HH+ttTvrcfNKPbuZecV6KT1B8I18BC5IURUh5kl4Yl5OEP5eFIUoI5BWxCsyYMhFsDx8kekw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.62.4.tgz", + "integrity": "sha512-zmfrQd/0wu6oJs8Vq8KwY/YtsKSsLtKe/HwAP4Wqy8LhWjeT55fHRAkOhYQ12wI3ayS4Tt12d5CDRD7N96SAYQ==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.62.4.tgz", + "integrity": "sha512-qPzHqdj9rfUD+w79dtE07zi/kFwKyCJqplp5K5ygeLTp7jLpAoc16OAH39HSmRC9UpozaecsleI8uAdEj6v2yw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.62.4.tgz", + "integrity": "sha512-zD6NdeWEByGE9QF9vCrlJ5YQB4oq9q91kPZS37Jwj5hOkvR1lTBSpsKhKDw4IJtbQ35LsTS1HD9DZYGKIshU1Q==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@types/estree": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", + "license": "MIT" + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/lodash": { + "version": "4.17.25", + "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.25.tgz", + "integrity": "sha512-+K1NIO8I+F9/wNulfVvu23QYd0Pe9/OCqRrim4NoYIf1VoEDL90Ve4ClzpyqBLc7NpGGWRvYNCKZ1BE/Jpf8dQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "22.20.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.20.1.tgz", + "integrity": "sha512-EANqOCF9QFyra+4pfxUcX9STKJpCLjMbObVzljIJomAWSnuSIEAvyzEU53GaajbXJEgdh0iEcPL+DGvpUd4k1Q==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/@types/parse-json": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.2.tgz", + "integrity": "sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw==", + "license": "MIT" + }, + "node_modules/@types/prop-types": { + "version": "15.7.15", + "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz", + "integrity": "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==", + "license": "MIT" + }, + "node_modules/@types/react": { + "version": "19.2.18", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.18.tgz", + "integrity": "sha512-AnzbBERsrLKtk2XSfTbYRLjQPdy116Sty4q+T+Bp3IC4l6jNBvreVPAHmpq9qhXQM7CXZPjLVmGMw9sy+hxQ3w==", + "license": "MIT", + "dependencies": { + "csstype": "^3.2.2" + } + }, + "node_modules/@types/react-dom": { + "version": "19.2.4", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.4.tgz", + "integrity": "sha512-Bsc+QHgp+P/F02XDzNCY9jnZNCUuLki36KT7VKrTXXLdHf+vHMNZnW1rVu5DNW/rCK+fya3DATySbLM4yhtKUw==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "^19.2.0" + } + }, + "node_modules/@types/react-transition-group": { + "version": "4.4.12", + "resolved": "https://registry.npmjs.org/@types/react-transition-group/-/react-transition-group-4.4.12.tgz", + "integrity": "sha512-8TV6R3h2j7a91c+1DXdJi3Syo69zzIZbz7Lg5tORM5LEJG7X/E6a1V3drRyBRZq7/utz7A+c4OgYLiLcYGHG6w==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*" + } + }, + "node_modules/@types/webidl-conversions": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/@types/webidl-conversions/-/webidl-conversions-7.0.3.tgz", + "integrity": "sha512-CiJJvcRtIgzadHCYXw7dqEnMNRjhGZlYK05Mj9OyktqV8uVT8fD2BFOB7S1uwBE3Kj2Z+4UyPmFw/Ixgw/LAlA==", + "license": "MIT" + }, + "node_modules/@types/whatwg-url": { + "version": "11.0.5", + "resolved": "https://registry.npmjs.org/@types/whatwg-url/-/whatwg-url-11.0.5.tgz", + "integrity": "sha512-coYR071JRaHa+xoEvvYqvnIHaVqaYrLPbsufM9BF63HkwI5Lgmy2QR8Q5K/lYDYo5AK82wOvSOS0UsLTpTG7uQ==", + "license": "MIT", + "dependencies": { + "@types/webidl-conversions": "*" + } + }, + "node_modules/accepts": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", + "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==", + "license": "MIT", + "dependencies": { + "mime-types": "^3.0.0", + "negotiator": "^1.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/acorn": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.18.0.tgz", + "integrity": "sha512-lGq+9yr1/GuAWaVYIHRjvvySG5/4VfKIvC8EWxStPdcDh/Ka7FG3twP6v4d5BkravUilhIAsG4Qj83t02LWUPQ==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/acorn-jsx-walk": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/acorn-jsx-walk/-/acorn-jsx-walk-2.0.0.tgz", + "integrity": "sha512-uuo6iJj4D4ygkdzd6jPtcxs8vZgDX9YFIkqczGImoypX2fQ4dVImmu3UzA4ynixCIMTrEOWW+95M2HuBaCEOVA==", + "dev": true, + "license": "MIT" + }, + "node_modules/acorn-loose": { + "version": "8.5.2", + "resolved": "https://registry.npmjs.org/acorn-loose/-/acorn-loose-8.5.2.tgz", + "integrity": "sha512-PPvV6g8UGMGgjrMu+n/f9E/tCSkNQ2Y97eFvuVdJfG11+xdIeDcLyNdC8SHcrHbRqkfwLASdplyR6B6sKM1U4A==", + "dev": true, + "license": "MIT", + "dependencies": { + "acorn": "^8.15.0" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-walk": { + "version": "8.3.5", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.5.tgz", + "integrity": "sha512-HEHNfbars9v4pgpW6SO1KSPkfoS0xVOM/9UzkJltjlsHZmJasxg8aXkuZa7SMf8vKGIBhpUsPluQSqhJFCqebw==", + "dev": true, + "license": "MIT", + "dependencies": { + "acorn": "^8.11.0" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/agent-base": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", + "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, + "node_modules/ajv": { + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz", + "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==", + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ajv-formats": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-3.0.1.tgz", + "integrity": "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==", + "license": "MIT", + "dependencies": { + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true, + "license": "Python-2.0" + }, + "node_modules/async-mutex": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/async-mutex/-/async-mutex-0.5.0.tgz", + "integrity": "sha512-1A94B18jkJ3DYq284ohPxoXbfTA5HsQ7/Mf4DEhcyLx3Bz27Rh59iScbB6EPiP+B+joue6YCxcMXSbFC1tZKwA==", + "dev": true, + "license": "MIT", + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/b4a": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/b4a/-/b4a-1.8.1.tgz", + "integrity": "sha512-aiqre1Nr0B/6DgE2N5vwTc+2/oQZ4Wh1t4NznYY4E00y8LCt6NqdRv81so00oo27D8MVKTpUa/MwUUtBLXCoDw==", + "dev": true, + "license": "Apache-2.0", + "peerDependencies": { + "react-native-b4a": "*" + }, + "peerDependenciesMeta": { + "react-native-b4a": { + "optional": true + } + } + }, + "node_modules/babel-plugin-macros": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/babel-plugin-macros/-/babel-plugin-macros-3.1.0.tgz", + "integrity": "sha512-Cg7TFGpIr01vOQNODXOOaGz2NpCU5gl8x1qJFbb6hbZxR7XrcE2vtbAsTAbJ7/xwJtUuJEw8K8Zr/AE0LHlesg==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.12.5", + "cosmiconfig": "^7.0.0", + "resolve": "^1.19.0" + }, + "engines": { + "node": ">=10", + "npm": ">=6" + } + }, + "node_modules/bare-events": { + "version": "2.9.1", + "resolved": "https://registry.npmjs.org/bare-events/-/bare-events-2.9.1.tgz", + "integrity": "sha512-Z0oHEHAFDZkffN8Qc39zNZjQlMDkPJRyyyZieU1VH7u8c5S+qHZ2S8ixdKIAxEjfHO7FJxXmJWgteOghVanIsg==", + "dev": true, + "license": "Apache-2.0", + "peerDependencies": { + "bare-abort-controller": "*" + }, + "peerDependenciesMeta": { + "bare-abort-controller": { + "optional": true + } + } + }, + "node_modules/bare-fs": { + "version": "4.8.0", + "resolved": "https://registry.npmjs.org/bare-fs/-/bare-fs-4.8.0.tgz", + "integrity": "sha512-fM+MhCvdQhZ7NV6S95a07gPSqjIYKn6mFaXfx266wN3ajZGl/+1AzH+ubkXQ0fFZvOe2nk9VHkzdYkQE5zMV3Q==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "bare-events": "^2.5.4", + "bare-path": "^3.0.0", + "bare-stream": "^2.6.4", + "bare-url": "^2.2.2", + "fast-fifo": "^1.3.2" + }, + "engines": { + "bare": ">=1.28.0" + }, + "peerDependencies": { + "bare-buffer": "*" + }, + "peerDependenciesMeta": { + "bare-buffer": { + "optional": true + } + } + }, + "node_modules/bare-path": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/bare-path/-/bare-path-3.1.1.tgz", + "integrity": "sha512-JprUlveX3QjApC1cTpsUOiscADftCGVWkzitbHsRqv84hzYwYHw2mbluddsq5TvI8mH/8Ov1f4BiMAdcB0oYnQ==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/bare-stream": { + "version": "2.13.3", + "resolved": "https://registry.npmjs.org/bare-stream/-/bare-stream-2.13.3.tgz", + "integrity": "sha512-Kc+brLqvEqGkjyfiwJmImAOqLZL7OsoLKuavx+hJjgVV3nLTOjloJyPMFxjUPerGGHrNH0fLU06jjykMLWrERQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "b4a": "^1.8.1", + "streamx": "^2.25.0", + "teex": "^1.0.1" + }, + "peerDependencies": { + "bare-abort-controller": "*", + "bare-buffer": "*", + "bare-events": "*" + }, + "peerDependenciesMeta": { + "bare-abort-controller": { + "optional": true + }, + "bare-buffer": { + "optional": true + }, + "bare-events": { + "optional": true + } + } + }, + "node_modules/bare-url": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/bare-url/-/bare-url-2.5.2.tgz", + "integrity": "sha512-L13PCJzKG8RGvx8V1/DdMi12ERhC3tprr7/8a94BxpmnRsFqxh5XZNdhtMxu5HPkRshYOOWRGY8lDP7ZhpG9Cg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "bare-path": "^3.0.0" + } + }, + "node_modules/body-parser": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.3.0.tgz", + "integrity": "sha512-2cGmJupaNgg+QUwVLAucDuWuoMZ6EX9iHDRswZ5lsNYEmwPaRknMPCLZz07yTzVq/83p4o/wzbDZbBrTvGGTIw==", + "license": "MIT", + "dependencies": { + "bytes": "^3.1.2", + "content-type": "^2.0.0", + "debug": "^4.4.3", + "http-errors": "^2.0.1", + "iconv-lite": "^0.7.2", + "on-finished": "^2.4.1", + "qs": "^6.15.2", + "raw-body": "^3.0.2", + "type-is": "^2.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/body-parser/node_modules/content-type": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-2.1.0.tgz", + "integrity": "sha512-mj7UPXE0jaqaOsukNZRUEfEi2AcL7C/vwmwcHV0O97eO1E1pxBZuyjlZrx5seTaNBg1U6+o35wpa35Qfcc+7ag==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/bson": { + "version": "6.10.4", + "resolved": "https://registry.npmjs.org/bson/-/bson-6.10.4.tgz", + "integrity": "sha512-WIsKqkSC0ABoBJuT1LEX+2HEvNmNKKgnTAyd0fL8qzK4SH2i9NXg+t08YtdZp/V9IZ33cxe3iV4yM0qg8lMQng==", + "license": "Apache-2.0", + "engines": { + "node": ">=16.20.1" + } + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/camelcase": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", + "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/chalk": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", "dev": true, "license": "MIT", "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/clsx": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/commander": { + "version": "13.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-13.1.0.tgz", + "integrity": "sha512-/rFeCpNJQbhSZjGVwO9RFV3xPqbnERS8MmIQzCtD/zl6gpJuV/bMLuN92oG3F7d8oDEHHRrujSXNUr8fpjntKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/commondir": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz", + "integrity": "sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg==", + "dev": true, + "license": "MIT" + }, + "node_modules/concurrently": { + "version": "9.2.4", + "resolved": "https://registry.npmjs.org/concurrently/-/concurrently-9.2.4.tgz", + "integrity": "sha512-TZ0CEhyzvFjgtAvHTusDMgj7wNdihCh7LLLrzdUOXIhdlnL2JBBGA9eJxR24rtqgmdjh3OA3hrN1rCHj6HM8qA==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "4.1.2", + "rxjs": "7.8.2", + "shell-quote": "1.9.0", + "supports-color": "8.1.1", + "tree-kill": "1.2.2", + "yargs": "17.7.2" + }, + "bin": { + "conc": "dist/bin/concurrently.js", + "concurrently": "dist/bin/concurrently.js" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/open-cli-tools/concurrently?sponsor=1" + } + }, + "node_modules/concurrently/node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/content-disposition": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.1.0.tgz", + "integrity": "sha512-5jRCH9Z/+DRP7rkvY83B+yGIGX96OYdJmzngqnw2SBSxqCFPd0w2km3s5iawpGX8krnwSGmF0FW5Nhr0Hfai3g==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/convert-source-map": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz", + "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==", + "license": "MIT" + }, + "node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", + "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==", + "license": "MIT", + "engines": { + "node": ">=6.6.0" + } + }, + "node_modules/cors": { + "version": "2.8.6", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.6.tgz", + "integrity": "sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==", + "license": "MIT", + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, + "engines": { + "node": ">= 0.10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/cosmiconfig": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-7.1.0.tgz", + "integrity": "sha512-AdmX6xUzdNASswsFtmwSt7Vj8po9IuqXm0UXz7QKPuEUmPB4XyjGfaAr2PSuELMwkRMVH1EpIkX5bTZGRB3eCA==", + "license": "MIT", + "dependencies": { + "@types/parse-json": "^4.0.0", + "import-fresh": "^3.2.1", + "parse-json": "^5.0.0", + "path-type": "^4.0.0", + "yaml": "^1.10.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/cosmiconfig/node_modules/yaml": { + "version": "1.10.3", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.3.tgz", + "integrity": "sha512-vIYeF1u3CjlhAFekPPAk2h/Kv4T3mAkMox5OymRiJQB0spDP10LHvt+K7G9Ny6NuuMAb25/6n1qyUjAcGNf/AA==", + "license": "ISC", + "engines": { + "node": ">= 6" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "license": "MIT" + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/dependency-cruiser": { + "version": "16.10.4", + "resolved": "https://registry.npmjs.org/dependency-cruiser/-/dependency-cruiser-16.10.4.tgz", + "integrity": "sha512-hrxVOjIm8idZ9ZVDGSyyG3SHiNcEUPhL6RTEmO/3wfQWLepH5pA3nuDMMrcJ1DkZztFA7xg3tk8OVO+MmwwH9w==", + "dev": true, + "license": "MIT", + "dependencies": { + "acorn": "^8.15.0", + "acorn-jsx": "^5.3.2", + "acorn-jsx-walk": "^2.0.0", + "acorn-loose": "^8.5.2", + "acorn-walk": "^8.3.4", + "ajv": "^8.17.1", + "commander": "^13.1.0", + "enhanced-resolve": "^5.18.2", + "ignore": "^7.0.5", + "interpret": "^3.1.1", + "is-installed-globally": "^1.0.0", + "json5": "^2.2.3", + "memoize": "^10.1.0", + "picocolors": "^1.1.1", + "picomatch": "^4.0.2", + "prompts": "^2.4.2", + "rechoir": "^0.8.0", + "safe-regex": "^2.1.1", + "semver": "^7.7.2", + "teamcity-service-messages": "^0.1.14", + "tsconfig-paths-webpack-plugin": "^4.2.0", + "watskeburt": "^4.2.3" + }, + "bin": { + "depcruise": "bin/dependency-cruise.mjs", + "depcruise-baseline": "bin/depcruise-baseline.mjs", + "depcruise-fmt": "bin/depcruise-fmt.mjs", + "depcruise-wrap-stream-in-html": "bin/wrap-stream-in-html.mjs", + "dependency-cruise": "bin/dependency-cruise.mjs", + "dependency-cruiser": "bin/dependency-cruise.mjs" + }, + "engines": { + "node": "^18.17||>=20" + } + }, + "node_modules/dom-helpers": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-5.2.1.tgz", + "integrity": "sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.8.7", + "csstype": "^3.0.2" + } + }, + "node_modules/dotenv": { + "version": "16.6.1", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz", + "integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "license": "MIT" + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/enhanced-resolve": { + "version": "5.24.5", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.24.5.tgz", + "integrity": "sha512-L1l8TNvomm6UVW5B253AGxQagSQr+vGwhMlrrfRS2qmhx46AMpMVJKQYLvWYbysTMY8VoicOvzHzoHMbyzB+4A==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.4", + "tapable": "^2.3.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/error-ex": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.4.tgz", + "integrity": "sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==", + "license": "MIT", + "dependencies": { + "is-arrayish": "^0.2.1" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.2.tgz", + "integrity": "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/esbuild": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.2.tgz", + "integrity": "sha512-HKVLS8dvII+xoKW9kmqxbRKrnWEXfJJr/FZhhJmiqIB0e053QNYFqOBouTMO/k5sID4MvCiUCvv8b9M4h32wIA==", + "devOptional": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.28.2", + "@esbuild/android-arm": "0.28.2", + "@esbuild/android-arm64": "0.28.2", + "@esbuild/android-x64": "0.28.2", + "@esbuild/darwin-arm64": "0.28.2", + "@esbuild/darwin-x64": "0.28.2", + "@esbuild/freebsd-arm64": "0.28.2", + "@esbuild/freebsd-x64": "0.28.2", + "@esbuild/linux-arm": "0.28.2", + "@esbuild/linux-arm64": "0.28.2", + "@esbuild/linux-ia32": "0.28.2", + "@esbuild/linux-loong64": "0.28.2", + "@esbuild/linux-mips64el": "0.28.2", + "@esbuild/linux-ppc64": "0.28.2", + "@esbuild/linux-riscv64": "0.28.2", + "@esbuild/linux-s390x": "0.28.2", + "@esbuild/linux-x64": "0.28.2", + "@esbuild/netbsd-arm64": "0.28.2", + "@esbuild/netbsd-x64": "0.28.2", + "@esbuild/openbsd-arm64": "0.28.2", + "@esbuild/openbsd-x64": "0.28.2", + "@esbuild/openharmony-arm64": "0.28.2", + "@esbuild/sunos-x64": "0.28.2", + "@esbuild/win32-arm64": "0.28.2", + "@esbuild/win32-ia32": "0.28.2", + "@esbuild/win32-x64": "0.28.2" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "license": "MIT" + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/events-universal": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/events-universal/-/events-universal-1.0.1.tgz", + "integrity": "sha512-LUd5euvbMLpwOF8m6ivPCbhQeSiYVNb8Vs0fQ8QjXo0JTkEHpz8pxdQf0gStltaPpw0Cca8b39KxvK9cfKRiAw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "bare-events": "^2.7.0" + } + }, + "node_modules/eventsource": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/eventsource/-/eventsource-3.0.7.tgz", + "integrity": "sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA==", + "license": "MIT", + "dependencies": { + "eventsource-parser": "^3.0.1" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/eventsource-parser": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/eventsource-parser/-/eventsource-parser-3.1.1.tgz", + "integrity": "sha512-EKN1vKAMcZ8MlYMpaNuxN6R9yakzH6uajHcHVTqWJzvu5pWw9DyhbP35HH8MVBQ+dZjAfDxk+A8NiR9KWaXiyQ==", + "license": "MIT", + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/express": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz", + "integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==", + "license": "MIT", + "dependencies": { + "accepts": "^2.0.0", + "body-parser": "^2.2.1", + "content-disposition": "^1.0.0", + "content-type": "^1.0.5", + "cookie": "^0.7.1", + "cookie-signature": "^1.2.1", + "debug": "^4.4.0", + "depd": "^2.0.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "finalhandler": "^2.1.0", + "fresh": "^2.0.0", + "http-errors": "^2.0.0", + "merge-descriptors": "^2.0.0", + "mime-types": "^3.0.0", + "on-finished": "^2.4.1", + "once": "^1.4.0", + "parseurl": "^1.3.3", + "proxy-addr": "^2.0.7", + "qs": "^6.14.0", + "range-parser": "^1.2.1", + "router": "^2.2.0", + "send": "^1.1.0", + "serve-static": "^2.2.0", + "statuses": "^2.0.1", + "type-is": "^2.0.1", + "vary": "^1.1.2" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/express-rate-limit": { + "version": "8.6.2", + "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-8.6.2.tgz", + "integrity": "sha512-YH4ru+eOJxQABscKFfRCy9R7x9QFGdezclVMwwgFFndzS2Xnm0uo6B0ABZsLhcpeptGv2qvuJVWlQr9gQZoC3A==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.3", + "ip-address": "^10.2.0" + }, + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://github.com/sponsors/express-rate-limit" + }, + "peerDependencies": { + "express": ">= 4.11" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "license": "MIT" + }, + "node_modules/fast-fifo": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/fast-fifo/-/fast-fifo-1.3.2.tgz", + "integrity": "sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-uri": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.5.tgz", + "integrity": "sha512-gHwA1O9LDIcKunMKhObS/HimwtehO1nPUECKAu5TpKgaO19fcWEl4bliWe1jWxVFvIXztJjjQ4L8XQ1EU9f7Jw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/finalhandler": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.1.tgz", + "integrity": "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "on-finished": "^2.4.1", + "parseurl": "^1.3.3", + "statuses": "^2.0.1" + }, + "engines": { + "node": ">= 18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/find-cache-dir": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-3.3.2.tgz", + "integrity": "sha512-wXZV5emFEjrridIgED11OoUKLxiYjAcqot/NJdAkOhlJ+vGzwhOAfcG5OX1jP+S0PcjEn8bdMJv+g2jwQ3Onig==", + "dev": true, + "license": "MIT", + "dependencies": { + "commondir": "^1.0.1", + "make-dir": "^3.0.2", + "pkg-dir": "^4.1.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/avajs/find-cache-dir?sponsor=1" + } + }, + "node_modules/find-root": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/find-root/-/find-root-1.1.0.tgz", + "integrity": "sha512-NKfW6bec6GfKc0SGx1e07QZY9PE99u0Bft/0rzSD5k3sO/vwkVUpDUKVm5Gpp5Ue3YfShPFTX2070tDs5kB9Ng==", + "license": "MIT" + }, + "node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/follow-redirects": { + "version": "1.16.0", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.16.0.tgz", + "integrity": "sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz", + "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true, + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/global-directory": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/global-directory/-/global-directory-4.0.1.tgz", + "integrity": "sha512-wHTUcDUoZ1H5/0iVqEudYW4/kAlN5cZ3j/bXn0Dpbizl9iaUVeWSHqiOjsgk6OW2bkLclbBjzewBz6weQ1zA2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ini": "4.1.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz", + "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/hoist-non-react-statics": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz", + "integrity": "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==", + "license": "BSD-3-Clause", + "dependencies": { + "react-is": "^16.7.0" + } + }, + "node_modules/hoist-non-react-statics/node_modules/react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", + "license": "MIT" + }, + "node_modules/hono": { + "version": "4.13.2", + "resolved": "https://registry.npmjs.org/hono/-/hono-4.13.2.tgz", + "integrity": "sha512-JydRilDRkYBQMt9qR9U92mXxmbGqsqSn/IKOrh4e7/gEbn+0zSr8igTu0obwJoNGN4sez28DIql7FBHWydoJpA==", + "license": "MIT", + "engines": { + "node": ">=16.9.0" + } + }, + "node_modules/http-errors": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", + "license": "MIT", + "dependencies": { + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" + }, + "engines": { + "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/https-proxy-agent": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/iconv-lite": { + "version": "0.7.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.3.tgz", + "integrity": "sha512-IKXpvIzjnC9XTAUbVBcMfGS0EPaIXtW6v+zr+RRp+hqULEpo0owZax6wyRwPOJbWbzjYspQwusTsfVr0ifh4uQ==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/ignore": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.6.tgz", + "integrity": "sha512-BAg6QkE8W+TuQLrrw0Ugr7HegXduRuuj8/ti2kSOc+jz1dmx8/WNcjr6XGnq5YpDWxFwwaavqD0+jIUOKelTsw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/import-fresh": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", + "license": "MIT", + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/ini": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/ini/-/ini-4.1.1.tgz", + "integrity": "sha512-QQnnxNyfvmHFIsj7gkPcYymR8Jdw/o7mp5ZFihxn6h8Ci6fh3Dx4E1gPjpQEpIuPo9XVNY/ZUwh4BPMjGyL01g==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/interpret": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/interpret/-/interpret-3.1.1.tgz", + "integrity": "sha512-6xwYfHbajpoF0xLW+iwLkhwgvLoZDfjYfoFNu8ftMoXINzwuymNLd9u/KmwtdT2GbR+/Cz66otEGEVVUHX9QLQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/ip-address": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.5.0.tgz", + "integrity": "sha512-R5SnVLJmgYYvf2F2ZgwSBnelz5G4q5AxIC277GDfUaNbrZKNANcBC7RHqYYePlszf4kBolVkJauG0ZjHHFh55g==", + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", + "license": "MIT" + }, + "node_modules/is-core-module": { + "version": "2.16.2", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.2.tgz", + "integrity": "sha512-evOr8xfXKxE6qSR0hSXL2r3sd7ALj8+7jQEUvPYcm5sgZFdJ+AYzT6yNmJenvIYQBgIGwfwz08sL8zoL7yq2BA==", + "license": "MIT", + "dependencies": { + "hasown": "^2.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-installed-globally": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-installed-globally/-/is-installed-globally-1.0.0.tgz", + "integrity": "sha512-K55T22lfpQ63N4KEN57jZUAaAYqYHEe8veb/TycJRk9DdSCLLcovXz/mL6mOnhQaZsQGwPhuFopdQIlqGSEjiQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "global-directory": "^4.0.1", + "is-path-inside": "^4.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-path-inside": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-4.0.0.tgz", + "integrity": "sha512-lJJV/5dYS+RcL8uQdBDW9c9uWFLLBNRyFhnAKXw5tVqLlKZ4RMGZKv+YQ/IA3OhD+RpbJa1LLFM1FQPGyIXvOA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-promise": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", + "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==", + "license": "MIT" + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "license": "ISC" + }, + "node_modules/jose": { + "version": "6.2.9", + "resolved": "https://registry.npmjs.org/jose/-/jose-6.2.9.tgz", + "integrity": "sha512-XrchZOFZUl/T3vTwRe8XK+cJrGtMF4th1ARnDfwbBXFKThGhlsxEE4Zu03AD/bjJSt/9jT/mxrOCkJWOg77aPA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/panva" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "license": "MIT" + }, + "node_modules/js-yaml": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.1.tgz", + "integrity": "sha512-CY6crGq313MX8GkwvB7tzgp99vjQxY1++5y10/BKN/GUfHqWaOGQMNZkBvqSzsZKWk/ijwHlWzzkLulsGHhjWQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/puzrin" + }, + { + "type": "github", + "url": "https://github.com/sponsors/nodeca" + } + ], + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", + "license": "MIT" + }, + "node_modules/json-schema-to-typescript": { + "version": "15.0.4", + "resolved": "https://registry.npmjs.org/json-schema-to-typescript/-/json-schema-to-typescript-15.0.4.tgz", + "integrity": "sha512-Su9oK8DR4xCmDsLlyvadkXzX6+GGXJpbhwoLtOGArAG61dvbW4YQmSEno2y66ahpIdmLMg6YUf/QHLgiwvkrHQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@apidevtools/json-schema-ref-parser": "^11.5.5", + "@types/json-schema": "^7.0.15", + "@types/lodash": "^4.17.7", + "is-glob": "^4.0.3", + "js-yaml": "^4.1.0", + "lodash": "^4.17.21", + "minimist": "^1.2.8", + "prettier": "^3.2.5", + "tinyglobby": "^0.2.9" + }, + "bin": { + "json2ts": "dist/src/cli.js" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "license": "MIT" + }, + "node_modules/json-schema-typed": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/json-schema-typed/-/json-schema-typed-8.0.2.tgz", + "integrity": "sha512-fQhoXdcvc3V28x7C7BMs4P5+kNlgUURe2jmUT1T//oBRMDrqy1QPelJimwZGo7Hg9VPV3EQV5Bnq4hbFy2vetA==", + "license": "BSD-2-Clause" + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/kleur": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz", + "integrity": "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "license": "MIT" + }, + "node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/lodash": { + "version": "4.18.1", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz", + "integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "license": "MIT", + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, + "node_modules/make-dir": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", + "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^6.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/make-dir/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/media-typer": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.1.tgz", + "integrity": "sha512-yz3xRaG20c6/BOzvYoDaGtPmGscs7YivItZEEqe6GbwNfHuxu9YNmvnEkMzKldAGY4/80pRcQRZSEnhquk9XuQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/memoize": { + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/memoize/-/memoize-10.2.0.tgz", + "integrity": "sha512-DeC6b7QBrZsRs3Y02A6A7lQyzFbsQbqgjI6UW0GigGWV+u1s25TycMr0XHZE4cJce7rY/vyw2ctMQqfDkIhUEA==", + "dev": true, + "license": "MIT", + "dependencies": { + "mimic-function": "^5.0.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sindresorhus/memoize?sponsor=1" + } + }, + "node_modules/memory-pager": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/memory-pager/-/memory-pager-1.5.0.tgz", + "integrity": "sha512-ZS4Bp4r/Zoeq6+NLJpP+0Zzm0pR8whtGPf1XExKLJBAczGMnSi3It14OiNCStjQjM6NU1okjQGSxgEZN8eBYKg==", + "license": "MIT" + }, + "node_modules/merge-descriptors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz", + "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz", + "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==", + "license": "MIT", + "dependencies": { + "mime-db": "^1.54.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/mimic-function": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/mimic-function/-/mimic-function-5.0.1.tgz", + "integrity": "sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/mongodb": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-6.21.0.tgz", + "integrity": "sha512-URyb/VXMjJ4da46OeSXg+puO39XH9DeQpWCslifrRn9JWugy0D+DvvBvkm2WxmHe61O/H19JM66p1z7RHVkZ6A==", + "license": "Apache-2.0", + "dependencies": { + "@mongodb-js/saslprep": "^1.3.0", + "bson": "^6.10.4", + "mongodb-connection-string-url": "^3.0.2" + }, + "engines": { + "node": ">=16.20.1" + }, + "peerDependencies": { + "@aws-sdk/credential-providers": "^3.188.0", + "@mongodb-js/zstd": "^1.1.0 || ^2.0.0", + "gcp-metadata": "^5.2.0", + "kerberos": "^2.0.1", + "mongodb-client-encryption": ">=6.0.0 <7", + "snappy": "^7.3.2", + "socks": "^2.7.1" + }, + "peerDependenciesMeta": { + "@aws-sdk/credential-providers": { + "optional": true + }, + "@mongodb-js/zstd": { + "optional": true + }, + "gcp-metadata": { + "optional": true + }, + "kerberos": { + "optional": true + }, + "mongodb-client-encryption": { + "optional": true + }, + "snappy": { + "optional": true + }, + "socks": { + "optional": true + } + } + }, + "node_modules/mongodb-connection-string-url": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/mongodb-connection-string-url/-/mongodb-connection-string-url-3.0.2.tgz", + "integrity": "sha512-rMO7CGo/9BFwyZABcKAWL8UJwH/Kc2x0g72uhDWzG48URRax5TCIcJ7Rc3RZqffZzO/Gwff/jyKwCU9TN8gehA==", + "license": "Apache-2.0", + "dependencies": { + "@types/whatwg-url": "^11.0.2", + "whatwg-url": "^14.1.0 || ^13.0.0" + } + }, + "node_modules/mongodb-memory-server": { + "version": "11.2.0", + "resolved": "https://registry.npmjs.org/mongodb-memory-server/-/mongodb-memory-server-11.2.0.tgz", + "integrity": "sha512-506AD8qvClVx8Raw/WhAUUWBgIXPyi856iC01aa5vAzHmn6WOXC6ulvudkTF7oTMzJxkyA0A84VpD4BpyfqJ9w==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "mongodb-memory-server-core": "11.2.0", + "tslib": "^2.8.1" + }, + "engines": { + "node": ">=20.19.0" + } + }, + "node_modules/mongodb-memory-server-core": { + "version": "11.2.0", + "resolved": "https://registry.npmjs.org/mongodb-memory-server-core/-/mongodb-memory-server-core-11.2.0.tgz", + "integrity": "sha512-vOoDtn0JiLrHvZY81Rp/UtKXXK0rtJHZGZFVnccvJwYitPLNspO0Ty0grqFQOe7iAET8+GI4zAQcphg+R3vxQg==", + "dev": true, + "license": "MIT", + "dependencies": { + "async-mutex": "^0.5.0", + "camelcase": "^6.3.0", + "debug": "^4.4.3", + "find-cache-dir": "^3.3.2", + "follow-redirects": "^1.16.0", + "https-proxy-agent": "^7.0.6", + "mongodb": "^7.2.0", + "new-find-package-json": "^2.0.0", + "semver": "^7.7.3", + "tar-stream": "^3.1.8", + "tslib": "^2.8.1", + "yauzl": "^3.3.1" + }, + "engines": { + "node": ">=20.19.0" + } + }, + "node_modules/mongodb-memory-server-core/node_modules/@types/whatwg-url": { + "version": "13.0.0", + "resolved": "https://registry.npmjs.org/@types/whatwg-url/-/whatwg-url-13.0.0.tgz", + "integrity": "sha512-N8WXpbE6Wgri7KUSvrmQcqrMllKZ9uxkYWMt+mCSGwNc0Hsw9VQTW7ApqI4XNrx6/SaM2QQJCzMPDEXE058s+Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/webidl-conversions": "*" + } + }, + "node_modules/mongodb-memory-server-core/node_modules/bson": { + "version": "7.3.2", + "resolved": "https://registry.npmjs.org/bson/-/bson-7.3.2.tgz", + "integrity": "sha512-1w0ra+ho1cuE+w8jzwgzTFIimFtCfZeCoOsvIPQg6uyFCsp8M29U7bNNf5GrFh88TXbYg1g3TyKUGpd6O7q6zA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=20.19.0" + } + }, + "node_modules/mongodb-memory-server-core/node_modules/mongodb": { + "version": "7.5.0", + "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-7.5.0.tgz", + "integrity": "sha512-5FnrEDLnvp6ycUOGLNLLU33BfCx2qmp2mJjGPDwKLruYsVzXVSK5fsGpoDXvsXJwBfBsD7ebMRdawbDxC2814g==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@mongodb-js/saslprep": "^1.4.11", + "bson": "^7.2.0", + "mongodb-connection-string-url": "^7.0.1" + }, + "engines": { + "node": ">=20.19.0" + }, + "peerDependencies": { + "@aws-sdk/credential-providers": "^3.806.0", + "@mongodb-js/zstd": "^7.0.0", + "gcp-metadata": "^7.0.1", + "kerberos": "^7.0.0", + "mongodb-client-encryption": "^7.2.0", + "snappy": "^7.3.2", + "socks": "^2.8.6" + }, + "peerDependenciesMeta": { + "@aws-sdk/credential-providers": { + "optional": true + }, + "@mongodb-js/zstd": { + "optional": true + }, + "gcp-metadata": { + "optional": true + }, + "kerberos": { + "optional": true + }, + "mongodb-client-encryption": { + "optional": true + }, + "snappy": { + "optional": true + }, + "socks": { + "optional": true + } + } + }, + "node_modules/mongodb-memory-server-core/node_modules/mongodb-connection-string-url": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/mongodb-connection-string-url/-/mongodb-connection-string-url-7.0.2.tgz", + "integrity": "sha512-ZoS07RoFqpKYQwAk59qmrx8+jJHNHU30UjlU96QktiGn1ltvDr+vCznLX5DiUBLEpMAHatHNWV1nM/74ul66kA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/whatwg-url": "^13.0.0", + "whatwg-url": "^14.1.0" + }, + "engines": { + "node": ">=20.19.0" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.18", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.18.tgz", + "integrity": "sha512-DTg4MJbGMWkfi6VZFdNt2/caMbQy4Ou+Op/hJQvGEWcnVfoA1QA+xzRKAzw9jD6+GVOOeYr/mIcuDSdug6F6+w==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/negotiator": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", + "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/new-find-package-json": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/new-find-package-json/-/new-find-package-json-2.0.0.tgz", + "integrity": "sha512-lDcBsjBSMlj3LXH2v/FW3txlh2pYTjmbOXPYJD93HI5EwuLzI11tdHSIpUMmfq/IOsldj4Ps8M8flhm+pCK4Ew==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.3.4" + }, + "engines": { + "node": ">=12.22.0" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "license": "MIT", + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/parse-json": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", + "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.0.0", + "error-ex": "^1.3.1", + "json-parse-even-better-errors": "^2.3.0", + "lines-and-columns": "^1.1.6" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "license": "MIT" + }, + "node_modules/path-to-regexp": { + "version": "8.4.2", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.4.2.tgz", + "integrity": "sha512-qRcuIdP69NPm4qbACK+aDogI5CBDMi1jKe0ry5rSQJz8JVLsC7jV8XpiJjGRLLol3N+R5ihGYcrPLTno6pAdBA==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/path-type": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", + "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/pend": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", + "integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==", + "dev": true, + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.5.tgz", + "integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pkce-challenge": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/pkce-challenge/-/pkce-challenge-5.0.1.tgz", + "integrity": "sha512-wQ0b/W4Fr01qtpHlqSqspcj3EhBvimsdh0KlHhH8HRZnMsEa0ea2fTULOXOS9ccQr3om+GcGRk4e+isrZWV8qQ==", + "license": "MIT", + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/pkg-dir": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", + "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "find-up": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/postcss": { + "version": "8.5.26", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.26.tgz", + "integrity": "sha512-u82N74LFzG8ca+dD8puPnplTXoGH4fTPpVGuIbt36G3qvNlkvfD0lEAZSxaly3KX8TS/L1A1gsCEmvKmBcVbkQ==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.17", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/prettier": { + "version": "3.9.6", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.9.6.tgz", + "integrity": "sha512-OpN0zzVdiaiAhxpuuj5efpIS4sY9j7bY6uR5mnj5yPzGkdkjNKSJeUThPb60Jw29QuAZgA4o+/iB49kFiaBX6g==", + "dev": true, + "license": "MIT", + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, + "node_modules/prompts": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz", + "integrity": "sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "kleur": "^3.0.3", + "sisteransi": "^1.0.5" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/prop-types": { + "version": "15.8.1", + "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", + "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.4.0", + "object-assign": "^4.1.1", + "react-is": "^16.13.1" + } + }, + "node_modules/prop-types/node_modules/react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", + "license": "MIT" + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "license": "MIT", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/qs": { + "version": "6.15.3", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.3.tgz", + "integrity": "sha512-O9gl3zCl5h5blw1KGUzQKhA5oUXSl8rwUIM5o0S3nCXMliSvy5Dzx7/DJcI+SwgICv+IneSZwhBh1oSyEHA71A==", + "license": "BSD-3-Clause", + "dependencies": { + "es-define-property": "^1.0.1", + "side-channel": "^1.1.1" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/range-parser": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.3.0.tgz", + "integrity": "sha512-hek2mFQpPuI4E1BBKrSto+BU3e3x4xuarsbiwr3+lf7p44juvFMV0XFWQAP3xUyqXA4RrXLIoaSUGbSt056ZMw==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/raw-body": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.2.tgz", + "integrity": "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==", + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "http-errors": "~2.0.1", + "iconv-lite": "~0.7.0", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/react": { + "version": "19.2.8", + "resolved": "https://registry.npmjs.org/react/-/react-19.2.8.tgz", + "integrity": "sha512-PWaYA1L/q9u2u7xYQi+Y3L3Yfnie7XyLeaJICV1MGD6LprsBxcAqGjYyr0eY3p+QdsA+x/Irkt4Qif8D63+Sbw==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "19.2.8", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.8.tgz", + "integrity": "sha512-rVprimfGBG3DR+Tq0IQG2DT5PxKth1WIGDmj5yPmlzr4YBe7uyE+Du4oVqTDXZSHGGGXRtTJEGSSePyQCMBglQ==", + "license": "MIT", + "dependencies": { + "scheduler": "^0.27.0" + }, + "peerDependencies": { + "react": "^19.2.8" + } + }, + "node_modules/react-is": { + "version": "19.2.8", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-19.2.8.tgz", + "integrity": "sha512-s5un28nYxKJw5gvUHyW5PCC28CvBqLu9r3cWgzHT4Vo/5fqqkFcdRYsGcKf50WMPpjjFZS5d76fn3YCo2njKwQ==", + "license": "MIT" + }, + "node_modules/react-transition-group": { + "version": "4.4.5", + "resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.5.tgz", + "integrity": "sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g==", + "license": "BSD-3-Clause", + "dependencies": { + "@babel/runtime": "^7.5.5", + "dom-helpers": "^5.0.1", + "loose-envify": "^1.4.0", + "prop-types": "^15.6.2" + }, + "peerDependencies": { + "react": ">=16.6.0", + "react-dom": ">=16.6.0" + } + }, + "node_modules/rechoir": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/rechoir/-/rechoir-0.8.0.tgz", + "integrity": "sha512-/vxpCXddiX8NGfGO/mTafwjq4aFa/71pvamip0++IQk3zG8cbCj0fifNPrjjF1XMXUne91jL9OoxmdykoEtifQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "resolve": "^1.20.0" + }, + "engines": { + "node": ">= 10.13.0" + } + }, + "node_modules/regexp-tree": { + "version": "0.1.27", + "resolved": "https://registry.npmjs.org/regexp-tree/-/regexp-tree-0.1.27.tgz", + "integrity": "sha512-iETxpjK6YoRWJG5o6hXLwvjYAoW+FEZn9os0PD/b6AP6xQwsa/Y7lCVgIixBbUPMfhu+i2LtdeAqVTgGlQarfA==", + "dev": true, + "license": "MIT", + "bin": { + "regexp-tree": "bin/regexp-tree" + } + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/resolve": { + "version": "1.22.12", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.12.tgz", + "integrity": "sha512-TyeJ1zif53BPfHootBGwPRYT1RUt6oGWsaQr8UyZW/eAm9bKoijtvruSDEmZHm92CwS9nj7/fWttqPCgzep8CA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "is-core-module": "^2.16.1", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/rollup": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.62.4.tgz", + "integrity": "sha512-RXOqwaPsBGjMNMa4sQjDjHieHEZDFoj/Rdr46l2MU5DfEs16wHJPC2RPTPHWhNl+M3aI472LLqFkFKut4SblOg==", + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.9" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@napi-rs/lzma-linux-x64-gnu": "1.5.1", + "@rollup/rollup-android-arm-eabi": "4.62.4", + "@rollup/rollup-android-arm64": "4.62.4", + "@rollup/rollup-darwin-arm64": "4.62.4", + "@rollup/rollup-darwin-x64": "4.62.4", + "@rollup/rollup-freebsd-arm64": "4.62.4", + "@rollup/rollup-freebsd-x64": "4.62.4", + "@rollup/rollup-linux-arm-gnueabihf": "4.62.4", + "@rollup/rollup-linux-arm-musleabihf": "4.62.4", + "@rollup/rollup-linux-arm64-gnu": "4.62.4", + "@rollup/rollup-linux-arm64-musl": "4.62.4", + "@rollup/rollup-linux-loong64-gnu": "4.62.4", + "@rollup/rollup-linux-loong64-musl": "4.62.4", + "@rollup/rollup-linux-ppc64-gnu": "4.62.4", + "@rollup/rollup-linux-ppc64-musl": "4.62.4", + "@rollup/rollup-linux-riscv64-gnu": "4.62.4", + "@rollup/rollup-linux-riscv64-musl": "4.62.4", + "@rollup/rollup-linux-s390x-gnu": "4.62.4", + "@rollup/rollup-linux-x64-gnu": "4.62.4", + "@rollup/rollup-linux-x64-musl": "4.62.4", + "@rollup/rollup-openbsd-x64": "4.62.4", + "@rollup/rollup-openharmony-arm64": "4.62.4", + "@rollup/rollup-win32-arm64-msvc": "4.62.4", + "@rollup/rollup-win32-ia32-msvc": "4.62.4", + "@rollup/rollup-win32-x64-gnu": "4.62.4", + "@rollup/rollup-win32-x64-msvc": "4.62.4", + "fsevents": "~2.3.2" + } + }, + "node_modules/router": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz", + "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "depd": "^2.0.0", + "is-promise": "^4.0.0", + "parseurl": "^1.3.3", + "path-to-regexp": "^8.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/rxjs": { + "version": "7.8.2", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.2.tgz", + "integrity": "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.1.0" + } + }, + "node_modules/safe-regex": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/safe-regex/-/safe-regex-2.1.1.tgz", + "integrity": "sha512-rx+x8AMzKb5Q5lQ95Zoi6ZbJqwCLkqi3XuJXp5P3rT8OEc6sZCJG5AE5dU3lsgRr/F4Bs31jSlVN+j5KrsGu9A==", + "dev": true, + "license": "MIT", + "dependencies": { + "regexp-tree": "~0.1.1" + } + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, + "node_modules/scheduler": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", + "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==", + "license": "MIT" + }, + "node_modules/semver": { + "version": "7.8.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/send": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/send/-/send-1.2.1.tgz", + "integrity": "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.3", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "fresh": "^2.0.0", + "http-errors": "^2.0.1", + "mime-types": "^3.0.2", + "ms": "^2.1.3", + "on-finished": "^2.4.1", + "range-parser": "^1.2.1", + "statuses": "^2.0.2" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/serve-static": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.1.tgz", + "integrity": "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==", + "license": "MIT", + "dependencies": { + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "parseurl": "^1.3.3", + "send": "^1.2.0" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "license": "ISC" + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/shell-quote": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.9.0.tgz", + "integrity": "sha512-Iov+JwFv/2HcTpcwNMKd8+IWNb8tboQJNQTkAY/LLVK7gGH9jy+LGkVqPxfekHl+yMmiqXszdGWXgkfml7hjqA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.1.tgz", + "integrity": "sha512-6x6dK6zJdpTzF4sQeNYxwtvBzf6Eg4GtlesS94HOvTudUeyK2WXAaIfmDgsyslYrRBeFIlsi54AYsFGUuhmvrQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.4", + "side-channel-list": "^1.0.1", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.1.tgz", + "integrity": "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/sisteransi": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", + "integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==", + "dev": true, + "license": "MIT" + }, + "node_modules/source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/sparse-bitfield": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/sparse-bitfield/-/sparse-bitfield-3.0.3.tgz", + "integrity": "sha512-kvzhi7vqKTfkh0PZU+2D2PIllw2ymqJKujUcyPMd9Y75Nv4nPbGJZXNhxsgdQab2BmlDct1YnfQCguEvHr7VsQ==", + "license": "MIT", + "dependencies": { + "memory-pager": "^1.0.2" + } + }, + "node_modules/statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/streamx": { + "version": "2.28.0", + "resolved": "https://registry.npmjs.org/streamx/-/streamx-2.28.0.tgz", + "integrity": "sha512-1Yowhzjf0ivGMrTIkY9hav5TxobO9qIVqUE41fiCGMGgc3CLlf4MY+9AHmZqBWgDTue0fY9zWjYFVyf6Diuobw==", + "dev": true, + "license": "MIT", + "dependencies": { + "events-universal": "^1.0.0", + "fast-fifo": "^1.3.2", + "text-decoder": "^1.1.0" + } + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" }, "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" + "node": ">=8" } }, - "node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", "dev": true, "license": "MIT", "dependencies": { - "color-name": "~1.1.4" + "ansi-regex": "^5.0.1" }, "engines": { - "node": ">=7.0.0" + "node": ">=8" } }, - "node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "node_modules/strip-bom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", + "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==", "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/stylis": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.2.0.tgz", + "integrity": "sha512-Orov6g6BB1sDfYgzWfTHDOxamtX1bE/zo104Dh9e6fqJ3PooipYyfJ0pUmrZO2wAvO8YbEyeFrkV91XTsGMSrw==", "license": "MIT" }, - "node_modules/commander": { - "version": "13.1.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-13.1.0.tgz", - "integrity": "sha512-/rFeCpNJQbhSZjGVwO9RFV3xPqbnERS8MmIQzCtD/zl6gpJuV/bMLuN92oG3F7d8oDEHHRrujSXNUr8fpjntKw==", + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", "dev": true, "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, "engines": { - "node": ">=18" + "node": ">=8" } }, - "node_modules/dependency-cruiser": { - "version": "16.10.4", - "resolved": "https://registry.npmjs.org/dependency-cruiser/-/dependency-cruiser-16.10.4.tgz", - "integrity": "sha512-hrxVOjIm8idZ9ZVDGSyyG3SHiNcEUPhL6RTEmO/3wfQWLepH5pA3nuDMMrcJ1DkZztFA7xg3tk8OVO+MmwwH9w==", + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/tapable": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.3.tgz", + "integrity": "sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/tar-stream": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-3.2.0.tgz", + "integrity": "sha512-ojzvCvVaNp6aOTFmG7jaRD0meowIAuPc3cMMhSgKiVWws1GyHbGd/xvnyuRKcKlMpt3qvxx6r0hreCNITP9hIg==", "dev": true, "license": "MIT", "dependencies": { - "acorn": "^8.15.0", - "acorn-jsx": "^5.3.2", - "acorn-jsx-walk": "^2.0.0", - "acorn-loose": "^8.5.2", - "acorn-walk": "^8.3.4", - "ajv": "^8.17.1", - "commander": "^13.1.0", - "enhanced-resolve": "^5.18.2", - "ignore": "^7.0.5", - "interpret": "^3.1.1", - "is-installed-globally": "^1.0.0", - "json5": "^2.2.3", - "memoize": "^10.1.0", - "picocolors": "^1.1.1", - "picomatch": "^4.0.2", - "prompts": "^2.4.2", - "rechoir": "^0.8.0", - "safe-regex": "^2.1.1", - "semver": "^7.7.2", - "teamcity-service-messages": "^0.1.14", - "tsconfig-paths-webpack-plugin": "^4.2.0", - "watskeburt": "^4.2.3" + "b4a": "^1.6.4", + "bare-fs": "^4.5.5", + "fast-fifo": "^1.2.0", + "streamx": "^2.15.0" + } + }, + "node_modules/teamcity-service-messages": { + "version": "0.1.14", + "resolved": "https://registry.npmjs.org/teamcity-service-messages/-/teamcity-service-messages-0.1.14.tgz", + "integrity": "sha512-29aQwaHqm8RMX74u2o/h1KbMLP89FjNiMxD9wbF2BbWOnbM+q+d1sCEC+MqCc4QW3NJykn77OMpTFw/xTHIc0w==", + "dev": true, + "license": "MIT" + }, + "node_modules/teex": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/teex/-/teex-1.0.1.tgz", + "integrity": "sha512-eYE6iEI62Ni1H8oIa7KlDU6uQBtqr4Eajni3wX7rpfXD8ysFx8z0+dri+KWEPWpBsxXfxu58x/0jvTVT1ekOSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "streamx": "^2.12.5" + } + }, + "node_modules/text-decoder": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/text-decoder/-/text-decoder-1.2.7.tgz", + "integrity": "sha512-vlLytXkeP4xvEq2otHeJfSQIRyWxo/oZGEbXrtEEF9Hnmrdly59sUbzZ/QgyWuLYHctCHxFF4tRQZNQ9k60ExQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "b4a": "^1.6.4" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.17", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", + "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/tr46": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-5.1.1.tgz", + "integrity": "sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==", + "license": "MIT", + "dependencies": { + "punycode": "^2.3.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/tree-kill": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz", + "integrity": "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==", + "dev": true, + "license": "MIT", "bin": { - "depcruise": "bin/dependency-cruise.mjs", - "depcruise-baseline": "bin/depcruise-baseline.mjs", - "depcruise-fmt": "bin/depcruise-fmt.mjs", - "depcruise-wrap-stream-in-html": "bin/wrap-stream-in-html.mjs", - "dependency-cruise": "bin/dependency-cruise.mjs", - "dependency-cruiser": "bin/dependency-cruise.mjs" + "tree-kill": "cli.js" + } + }, + "node_modules/tsconfig-paths": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-4.2.0.tgz", + "integrity": "sha512-NoZ4roiN7LnbKn9QqE1amc9DJfzvZXxF4xDavcOWt1BPkdx+m+0gJuPM+S0vCe7zTJMYUP0R8pO2XMr+Y8oLIg==", + "dev": true, + "license": "MIT", + "dependencies": { + "json5": "^2.2.2", + "minimist": "^1.2.6", + "strip-bom": "^3.0.0" }, "engines": { - "node": "^18.17||>=20" + "node": ">=6" } }, - "node_modules/enhanced-resolve": { - "version": "5.24.5", - "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.24.5.tgz", - "integrity": "sha512-L1l8TNvomm6UVW5B253AGxQagSQr+vGwhMlrrfRS2qmhx46AMpMVJKQYLvWYbysTMY8VoicOvzHzoHMbyzB+4A==", + "node_modules/tsconfig-paths-webpack-plugin": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/tsconfig-paths-webpack-plugin/-/tsconfig-paths-webpack-plugin-4.2.0.tgz", + "integrity": "sha512-zbem3rfRS8BgeNK50Zz5SIQgXzLafiHjOwUAvk/38/o1jHn/V5QAgVUcz884or7WYcPaH3N2CIfUc2u0ul7UcA==", "dev": true, "license": "MIT", "dependencies": { - "graceful-fs": "^4.2.4", - "tapable": "^2.3.3" + "chalk": "^4.1.0", + "enhanced-resolve": "^5.7.0", + "tapable": "^2.2.1", + "tsconfig-paths": "^4.1.2" }, "engines": { "node": ">=10.13.0" } }, - "node_modules/es-errors": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", - "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "dev": true, + "license": "0BSD" + }, + "node_modules/tsx": { + "version": "4.23.12", + "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.23.12.tgz", + "integrity": "sha512-FDf4L4sYzKtzWYhU/Xm0AQFdTjdIxNo9ElTf2mxXM6k8YMHXzYUe4yODVaXP4V9uMFbVg8c0qyBccK2OOxb45Q==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "esbuild": "~0.28.0" + }, + "bin": { + "tsx": "dist/cli.mjs" + }, + "engines": { + "node": ">=18.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + } + }, + "node_modules/type-is": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.1.0.tgz", + "integrity": "sha512-faYHw0anBbc/kWF3zFTEnxSFOAGUX9GFbOBthvDdLsIlEoWOFOtS0zgCiQYwIskL9iGXZL3kAXD8OoZ4GmMATA==", + "license": "MIT", + "dependencies": { + "content-type": "^2.0.0", + "media-typer": "^1.1.0", + "mime-types": "^3.0.0" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/type-is/node_modules/content-type": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-2.1.0.tgz", + "integrity": "sha512-mj7UPXE0jaqaOsukNZRUEfEi2AcL7C/vwmwcHV0O97eO1E1pxBZuyjlZrx5seTaNBg1U6+o35wpa35Qfcc+7ag==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", "license": "MIT", "engines": { - "node": ">= 0.4" + "node": ">= 0.8" } }, - "node_modules/esbuild": { - "version": "0.28.2", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.2.tgz", - "integrity": "sha512-HKVLS8dvII+xoKW9kmqxbRKrnWEXfJJr/FZhhJmiqIB0e053QNYFqOBouTMO/k5sID4MvCiUCvv8b9M4h32wIA==", - "dev": true, - "hasInstallScript": true, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", "license": "MIT", - "bin": { - "esbuild": "bin/esbuild" - }, "engines": { - "node": ">=18" - }, - "optionalDependencies": { - "@esbuild/aix-ppc64": "0.28.2", - "@esbuild/android-arm": "0.28.2", - "@esbuild/android-arm64": "0.28.2", - "@esbuild/android-x64": "0.28.2", - "@esbuild/darwin-arm64": "0.28.2", - "@esbuild/darwin-x64": "0.28.2", - "@esbuild/freebsd-arm64": "0.28.2", - "@esbuild/freebsd-x64": "0.28.2", - "@esbuild/linux-arm": "0.28.2", - "@esbuild/linux-arm64": "0.28.2", - "@esbuild/linux-ia32": "0.28.2", - "@esbuild/linux-loong64": "0.28.2", - "@esbuild/linux-mips64el": "0.28.2", - "@esbuild/linux-ppc64": "0.28.2", - "@esbuild/linux-riscv64": "0.28.2", - "@esbuild/linux-s390x": "0.28.2", - "@esbuild/linux-x64": "0.28.2", - "@esbuild/netbsd-arm64": "0.28.2", - "@esbuild/netbsd-x64": "0.28.2", - "@esbuild/openbsd-arm64": "0.28.2", - "@esbuild/openbsd-x64": "0.28.2", - "@esbuild/openharmony-arm64": "0.28.2", - "@esbuild/sunos-x64": "0.28.2", - "@esbuild/win32-arm64": "0.28.2", - "@esbuild/win32-ia32": "0.28.2", - "@esbuild/win32-x64": "0.28.2" + "node": ">= 0.8" } }, - "node_modules/fast-deep-equal": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", - "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", - "dev": true, - "license": "MIT" - }, - "node_modules/fast-uri": { - "version": "3.1.5", - "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.5.tgz", - "integrity": "sha512-gHwA1O9LDIcKunMKhObS/HimwtehO1nPUECKAu5TpKgaO19fcWEl4bliWe1jWxVFvIXztJjjQ4L8XQ1EU9f7Jw==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/fastify" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/fastify" - } - ], - "license": "BSD-3-Clause" - }, - "node_modules/fdir": { - "version": "6.5.0", - "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", - "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", - "dev": true, + "node_modules/vite": { + "version": "6.4.3", + "resolved": "https://registry.npmjs.org/vite/-/vite-6.4.3.tgz", + "integrity": "sha512-NTKlcQjlAK7MlQoyb6LgaqHc8sso/pVyUJYWMws3jg21uTJw/LddqIFPcPqP6PzpgbIcZyKI85sFE4HBrQDA8A==", "license": "MIT", + "dependencies": { + "esbuild": "^0.25.0", + "fdir": "^6.4.4", + "picomatch": "^4.0.2", + "postcss": "^8.5.3", + "rollup": "^4.34.9", + "tinyglobby": "^0.2.13" + }, + "bin": { + "vite": "bin/vite.js" + }, "engines": { - "node": ">=12.0.0" + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" }, "peerDependencies": { - "picomatch": "^3 || ^4" + "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", + "jiti": ">=1.21.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" }, "peerDependenciesMeta": { - "picomatch": { + "@types/node": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { "optional": true } } }, - "node_modules/fsevents": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", - "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", - "dev": true, - "hasInstallScript": true, + "node_modules/vite/node_modules/@esbuild/aix-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz", + "integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==", + "cpu": [ + "ppc64" + ], "license": "MIT", "optional": true, "os": [ - "darwin" + "aix" ], "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + "node": ">=18" } }, - "node_modules/function-bind": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", - "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", - "dev": true, + "node_modules/vite/node_modules/@esbuild/android-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.12.tgz", + "integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==", + "cpu": [ + "arm" + ], "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/ljharb" + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" } }, - "node_modules/global-directory": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/global-directory/-/global-directory-4.0.1.tgz", - "integrity": "sha512-wHTUcDUoZ1H5/0iVqEudYW4/kAlN5cZ3j/bXn0Dpbizl9iaUVeWSHqiOjsgk6OW2bkLclbBjzewBz6weQ1zA2Q==", - "dev": true, + "node_modules/vite/node_modules/@esbuild/android-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz", + "integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==", + "cpu": [ + "arm64" + ], "license": "MIT", - "dependencies": { - "ini": "4.1.1" - }, + "optional": true, + "os": [ + "android" + ], "engines": { "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/graceful-fs": { - "version": "4.2.11", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", - "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", - "dev": true, - "license": "ISC" - }, - "node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true, + "node_modules/vite/node_modules/@esbuild/android-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.12.tgz", + "integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==", + "cpu": [ + "x64" + ], "license": "MIT", + "optional": true, + "os": [ + "android" + ], "engines": { - "node": ">=8" + "node": ">=18" } }, - "node_modules/hasown": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz", - "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==", - "dev": true, + "node_modules/vite/node_modules/@esbuild/darwin-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz", + "integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==", + "cpu": [ + "arm64" + ], "license": "MIT", - "dependencies": { - "function-bind": "^1.1.2" - }, + "optional": true, + "os": [ + "darwin" + ], "engines": { - "node": ">= 0.4" + "node": ">=18" } }, - "node_modules/ignore": { - "version": "7.0.6", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.6.tgz", - "integrity": "sha512-BAg6QkE8W+TuQLrrw0Ugr7HegXduRuuj8/ti2kSOc+jz1dmx8/WNcjr6XGnq5YpDWxFwwaavqD0+jIUOKelTsw==", - "dev": true, + "node_modules/vite/node_modules/@esbuild/darwin-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz", + "integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==", + "cpu": [ + "x64" + ], "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], "engines": { - "node": ">= 4" + "node": ">=18" } }, - "node_modules/ini": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/ini/-/ini-4.1.1.tgz", - "integrity": "sha512-QQnnxNyfvmHFIsj7gkPcYymR8Jdw/o7mp5ZFihxn6h8Ci6fh3Dx4E1gPjpQEpIuPo9XVNY/ZUwh4BPMjGyL01g==", - "dev": true, - "license": "ISC", + "node_modules/vite/node_modules/@esbuild/freebsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz", + "integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + "node": ">=18" } }, - "node_modules/interpret": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/interpret/-/interpret-3.1.1.tgz", - "integrity": "sha512-6xwYfHbajpoF0xLW+iwLkhwgvLoZDfjYfoFNu8ftMoXINzwuymNLd9u/KmwtdT2GbR+/Cz66otEGEVVUHX9QLQ==", - "dev": true, + "node_modules/vite/node_modules/@esbuild/freebsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz", + "integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==", + "cpu": [ + "x64" + ], "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], "engines": { - "node": ">=10.13.0" + "node": ">=18" } }, - "node_modules/is-core-module": { - "version": "2.16.2", - "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.2.tgz", - "integrity": "sha512-evOr8xfXKxE6qSR0hSXL2r3sd7ALj8+7jQEUvPYcm5sgZFdJ+AYzT6yNmJenvIYQBgIGwfwz08sL8zoL7yq2BA==", - "dev": true, + "node_modules/vite/node_modules/@esbuild/linux-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz", + "integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==", + "cpu": [ + "arm" + ], "license": "MIT", - "dependencies": { - "hasown": "^2.0.3" - }, + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "node": ">=18" } }, - "node_modules/is-extglob": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", - "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", - "dev": true, + "node_modules/vite/node_modules/@esbuild/linux-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz", + "integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==", + "cpu": [ + "arm64" + ], "license": "MIT", + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": ">=0.10.0" + "node": ">=18" } }, - "node_modules/is-glob": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", - "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", - "dev": true, + "node_modules/vite/node_modules/@esbuild/linux-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz", + "integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==", + "cpu": [ + "ia32" + ], "license": "MIT", - "dependencies": { - "is-extglob": "^2.1.1" - }, + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": ">=0.10.0" + "node": ">=18" } }, - "node_modules/is-installed-globally": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-installed-globally/-/is-installed-globally-1.0.0.tgz", - "integrity": "sha512-K55T22lfpQ63N4KEN57jZUAaAYqYHEe8veb/TycJRk9DdSCLLcovXz/mL6mOnhQaZsQGwPhuFopdQIlqGSEjiQ==", - "dev": true, + "node_modules/vite/node_modules/@esbuild/linux-loong64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz", + "integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==", + "cpu": [ + "loong64" + ], "license": "MIT", - "dependencies": { - "global-directory": "^4.0.1", - "is-path-inside": "^4.0.0" - }, + "optional": true, + "os": [ + "linux" + ], "engines": { "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/is-path-inside": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-4.0.0.tgz", - "integrity": "sha512-lJJV/5dYS+RcL8uQdBDW9c9uWFLLBNRyFhnAKXw5tVqLlKZ4RMGZKv+YQ/IA3OhD+RpbJa1LLFM1FQPGyIXvOA==", - "dev": true, + "node_modules/vite/node_modules/@esbuild/linux-mips64el": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz", + "integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==", + "cpu": [ + "mips64el" + ], "license": "MIT", + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "node": ">=18" } }, - "node_modules/js-yaml": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.1.tgz", - "integrity": "sha512-CY6crGq313MX8GkwvB7tzgp99vjQxY1++5y10/BKN/GUfHqWaOGQMNZkBvqSzsZKWk/ijwHlWzzkLulsGHhjWQ==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/puzrin" - }, - { - "type": "github", - "url": "https://github.com/sponsors/nodeca" - } + "node_modules/vite/node_modules/@esbuild/linux-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz", + "integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==", + "cpu": [ + "ppc64" ], "license": "MIT", - "dependencies": { - "argparse": "^2.0.1" - }, - "bin": { - "js-yaml": "bin/js-yaml.js" + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" } }, - "node_modules/json-schema-to-typescript": { - "version": "15.0.4", - "resolved": "https://registry.npmjs.org/json-schema-to-typescript/-/json-schema-to-typescript-15.0.4.tgz", - "integrity": "sha512-Su9oK8DR4xCmDsLlyvadkXzX6+GGXJpbhwoLtOGArAG61dvbW4YQmSEno2y66ahpIdmLMg6YUf/QHLgiwvkrHQ==", - "dev": true, + "node_modules/vite/node_modules/@esbuild/linux-riscv64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz", + "integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==", + "cpu": [ + "riscv64" + ], "license": "MIT", - "dependencies": { - "@apidevtools/json-schema-ref-parser": "^11.5.5", - "@types/json-schema": "^7.0.15", - "@types/lodash": "^4.17.7", - "is-glob": "^4.0.3", - "js-yaml": "^4.1.0", - "lodash": "^4.17.21", - "minimist": "^1.2.8", - "prettier": "^3.2.5", - "tinyglobby": "^0.2.9" - }, - "bin": { - "json2ts": "dist/src/cli.js" - }, + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": ">=16.0.0" + "node": ">=18" } }, - "node_modules/json-schema-traverse": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", - "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", - "dev": true, - "license": "MIT" - }, - "node_modules/json5": { - "version": "2.2.3", - "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", - "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", - "dev": true, + "node_modules/vite/node_modules/@esbuild/linux-s390x": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz", + "integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==", + "cpu": [ + "s390x" + ], "license": "MIT", - "bin": { - "json5": "lib/cli.js" - }, + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": ">=6" + "node": ">=18" } }, - "node_modules/kleur": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz", - "integrity": "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==", - "dev": true, + "node_modules/vite/node_modules/@esbuild/linux-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz", + "integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==", + "cpu": [ + "x64" + ], "license": "MIT", + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": ">=6" + "node": ">=18" } }, - "node_modules/lodash": { - "version": "4.18.1", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz", - "integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==", - "dev": true, - "license": "MIT" - }, - "node_modules/memoize": { - "version": "10.2.0", - "resolved": "https://registry.npmjs.org/memoize/-/memoize-10.2.0.tgz", - "integrity": "sha512-DeC6b7QBrZsRs3Y02A6A7lQyzFbsQbqgjI6UW0GigGWV+u1s25TycMr0XHZE4cJce7rY/vyw2ctMQqfDkIhUEA==", - "dev": true, + "node_modules/vite/node_modules/@esbuild/netbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz", + "integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==", + "cpu": [ + "arm64" + ], "license": "MIT", - "dependencies": { - "mimic-function": "^5.0.1" - }, + "optional": true, + "os": [ + "netbsd" + ], "engines": { "node": ">=18" - }, - "funding": { - "url": "https://github.com/sindresorhus/memoize?sponsor=1" } }, - "node_modules/mimic-function": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/mimic-function/-/mimic-function-5.0.1.tgz", - "integrity": "sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==", - "dev": true, + "node_modules/vite/node_modules/@esbuild/netbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz", + "integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==", + "cpu": [ + "x64" + ], "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], "engines": { "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/minimist": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", - "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", - "dev": true, + "node_modules/vite/node_modules/@esbuild/openbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz", + "integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==", + "cpu": [ + "arm64" + ], "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/ljharb" + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" } }, - "node_modules/path-parse": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", - "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", - "dev": true, - "license": "MIT" - }, - "node_modules/picocolors": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", - "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", - "dev": true, - "license": "ISC" - }, - "node_modules/picomatch": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.5.tgz", - "integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==", - "dev": true, + "node_modules/vite/node_modules/@esbuild/openbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz", + "integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==", + "cpu": [ + "x64" + ], "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" + "node": ">=18" } }, - "node_modules/prettier": { - "version": "3.9.6", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.9.6.tgz", - "integrity": "sha512-OpN0zzVdiaiAhxpuuj5efpIS4sY9j7bY6uR5mnj5yPzGkdkjNKSJeUThPb60Jw29QuAZgA4o+/iB49kFiaBX6g==", - "dev": true, + "node_modules/vite/node_modules/@esbuild/openharmony-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz", + "integrity": "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==", + "cpu": [ + "arm64" + ], "license": "MIT", - "bin": { - "prettier": "bin/prettier.cjs" - }, + "optional": true, + "os": [ + "openharmony" + ], "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/prettier/prettier?sponsor=1" + "node": ">=18" } }, - "node_modules/prompts": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz", - "integrity": "sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==", - "dev": true, + "node_modules/vite/node_modules/@esbuild/sunos-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz", + "integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==", + "cpu": [ + "x64" + ], "license": "MIT", - "dependencies": { - "kleur": "^3.0.3", - "sisteransi": "^1.0.5" - }, + "optional": true, + "os": [ + "sunos" + ], "engines": { - "node": ">= 6" + "node": ">=18" } }, - "node_modules/rechoir": { - "version": "0.8.0", - "resolved": "https://registry.npmjs.org/rechoir/-/rechoir-0.8.0.tgz", - "integrity": "sha512-/vxpCXddiX8NGfGO/mTafwjq4aFa/71pvamip0++IQk3zG8cbCj0fifNPrjjF1XMXUne91jL9OoxmdykoEtifQ==", - "dev": true, + "node_modules/vite/node_modules/@esbuild/win32-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz", + "integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==", + "cpu": [ + "arm64" + ], "license": "MIT", - "dependencies": { - "resolve": "^1.20.0" - }, + "optional": true, + "os": [ + "win32" + ], "engines": { - "node": ">= 10.13.0" + "node": ">=18" } }, - "node_modules/regexp-tree": { - "version": "0.1.27", - "resolved": "https://registry.npmjs.org/regexp-tree/-/regexp-tree-0.1.27.tgz", - "integrity": "sha512-iETxpjK6YoRWJG5o6hXLwvjYAoW+FEZn9os0PD/b6AP6xQwsa/Y7lCVgIixBbUPMfhu+i2LtdeAqVTgGlQarfA==", - "dev": true, + "node_modules/vite/node_modules/@esbuild/win32-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz", + "integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==", + "cpu": [ + "ia32" + ], "license": "MIT", - "bin": { - "regexp-tree": "bin/regexp-tree" + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" } }, - "node_modules/require-from-string": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", - "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", - "dev": true, + "node_modules/vite/node_modules/@esbuild/win32-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz", + "integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==", + "cpu": [ + "x64" + ], "license": "MIT", + "optional": true, + "os": [ + "win32" + ], "engines": { - "node": ">=0.10.0" + "node": ">=18" } }, - "node_modules/resolve": { - "version": "1.22.12", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.12.tgz", - "integrity": "sha512-TyeJ1zif53BPfHootBGwPRYT1RUt6oGWsaQr8UyZW/eAm9bKoijtvruSDEmZHm92CwS9nj7/fWttqPCgzep8CA==", - "dev": true, + "node_modules/vite/node_modules/esbuild": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz", + "integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==", + "hasInstallScript": true, "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "is-core-module": "^2.16.1", - "path-parse": "^1.0.7", - "supports-preserve-symlinks-flag": "^1.0.0" - }, "bin": { - "resolve": "bin/resolve" + "esbuild": "bin/esbuild" }, "engines": { - "node": ">= 0.4" + "node": ">=18" }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.25.12", + "@esbuild/android-arm": "0.25.12", + "@esbuild/android-arm64": "0.25.12", + "@esbuild/android-x64": "0.25.12", + "@esbuild/darwin-arm64": "0.25.12", + "@esbuild/darwin-x64": "0.25.12", + "@esbuild/freebsd-arm64": "0.25.12", + "@esbuild/freebsd-x64": "0.25.12", + "@esbuild/linux-arm": "0.25.12", + "@esbuild/linux-arm64": "0.25.12", + "@esbuild/linux-ia32": "0.25.12", + "@esbuild/linux-loong64": "0.25.12", + "@esbuild/linux-mips64el": "0.25.12", + "@esbuild/linux-ppc64": "0.25.12", + "@esbuild/linux-riscv64": "0.25.12", + "@esbuild/linux-s390x": "0.25.12", + "@esbuild/linux-x64": "0.25.12", + "@esbuild/netbsd-arm64": "0.25.12", + "@esbuild/netbsd-x64": "0.25.12", + "@esbuild/openbsd-arm64": "0.25.12", + "@esbuild/openbsd-x64": "0.25.12", + "@esbuild/openharmony-arm64": "0.25.12", + "@esbuild/sunos-x64": "0.25.12", + "@esbuild/win32-arm64": "0.25.12", + "@esbuild/win32-ia32": "0.25.12", + "@esbuild/win32-x64": "0.25.12" } }, - "node_modules/safe-regex": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/safe-regex/-/safe-regex-2.1.1.tgz", - "integrity": "sha512-rx+x8AMzKb5Q5lQ95Zoi6ZbJqwCLkqi3XuJXp5P3rT8OEc6sZCJG5AE5dU3lsgRr/F4Bs31jSlVN+j5KrsGu9A==", + "node_modules/watskeburt": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/watskeburt/-/watskeburt-4.2.3.tgz", + "integrity": "sha512-uG9qtQYoHqAsnT711nG5iZc/8M5inSmkGCOp7pFaytKG2aTfIca7p//CjiVzAE4P7hzaYuCozMjNNaLgmhbK5g==", "dev": true, "license": "MIT", - "dependencies": { - "regexp-tree": "~0.1.1" - } - }, - "node_modules/semver": { - "version": "7.8.5", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", - "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", - "dev": true, - "license": "ISC", "bin": { - "semver": "bin/semver.js" + "watskeburt": "dist/run-cli.js" }, "engines": { - "node": ">=10" + "node": "^18||>=20" } }, - "node_modules/sisteransi": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", - "integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==", - "dev": true, - "license": "MIT" - }, - "node_modules/strip-bom": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", - "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==", - "dev": true, - "license": "MIT", + "node_modules/webidl-conversions": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", + "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==", + "license": "BSD-2-Clause", "engines": { - "node": ">=4" + "node": ">=12" } }, - "node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, + "node_modules/whatwg-url": { + "version": "14.2.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-14.2.0.tgz", + "integrity": "sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==", "license": "MIT", "dependencies": { - "has-flag": "^4.0.0" + "tr46": "^5.1.0", + "webidl-conversions": "^7.0.0" }, "engines": { - "node": ">=8" + "node": ">=18" } }, - "node_modules/supports-preserve-symlinks-flag": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", - "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" } }, - "node_modules/tapable": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.3.tgz", - "integrity": "sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A==", + "node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", "dev": true, "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, "engines": { - "node": ">=6" + "node": ">=10" }, "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, - "node_modules/teamcity-service-messages": { - "version": "0.1.14", - "resolved": "https://registry.npmjs.org/teamcity-service-messages/-/teamcity-service-messages-0.1.14.tgz", - "integrity": "sha512-29aQwaHqm8RMX74u2o/h1KbMLP89FjNiMxD9wbF2BbWOnbM+q+d1sCEC+MqCc4QW3NJykn77OMpTFw/xTHIc0w==", - "dev": true, - "license": "MIT" + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "license": "ISC" }, - "node_modules/tinyglobby": { - "version": "0.2.17", - "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", - "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", "dev": true, - "license": "MIT", - "dependencies": { - "fdir": "^6.5.0", - "picomatch": "^4.0.4" + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/yaml": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.9.0.tgz", + "integrity": "sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA==", + "license": "ISC", + "optional": true, + "peer": true, + "bin": { + "yaml": "bin.mjs" }, "engines": { - "node": ">=12.0.0" + "node": ">= 14.6" }, "funding": { - "url": "https://github.com/sponsors/SuperchupuDev" + "url": "https://github.com/sponsors/eemeli" } }, - "node_modules/tsconfig-paths": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-4.2.0.tgz", - "integrity": "sha512-NoZ4roiN7LnbKn9QqE1amc9DJfzvZXxF4xDavcOWt1BPkdx+m+0gJuPM+S0vCe7zTJMYUP0R8pO2XMr+Y8oLIg==", + "node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", "dev": true, "license": "MIT", "dependencies": { - "json5": "^2.2.2", - "minimist": "^1.2.6", - "strip-bom": "^3.0.0" + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" }, "engines": { - "node": ">=6" + "node": ">=12" } }, - "node_modules/tsconfig-paths-webpack-plugin": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/tsconfig-paths-webpack-plugin/-/tsconfig-paths-webpack-plugin-4.2.0.tgz", - "integrity": "sha512-zbem3rfRS8BgeNK50Zz5SIQgXzLafiHjOwUAvk/38/o1jHn/V5QAgVUcz884or7WYcPaH3N2CIfUc2u0ul7UcA==", + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", "dev": true, - "license": "MIT", - "dependencies": { - "chalk": "^4.1.0", - "enhanced-resolve": "^5.7.0", - "tapable": "^2.2.1", - "tsconfig-paths": "^4.1.2" - }, + "license": "ISC", "engines": { - "node": ">=10.13.0" + "node": ">=12" } }, - "node_modules/tsx": { - "version": "4.23.12", - "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.23.12.tgz", - "integrity": "sha512-FDf4L4sYzKtzWYhU/Xm0AQFdTjdIxNo9ElTf2mxXM6k8YMHXzYUe4yODVaXP4V9uMFbVg8c0qyBccK2OOxb45Q==", + "node_modules/yauzl": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-3.4.0.tgz", + "integrity": "sha512-jIH9yLR9wqr0wOS0TpBvo/g/2UgZH5qePVbjgRliiF0BYvOZyaBknKsF+x9Iht0O6sqgnB93rCICdOZFecJuDw==", "dev": true, "license": "MIT", "dependencies": { - "esbuild": "~0.28.0" - }, - "bin": { - "tsx": "dist/cli.mjs" + "pend": "~1.2.0" }, "engines": { - "node": ">=18.0.0" - }, - "optionalDependencies": { - "fsevents": "~2.3.3" + "node": ">=12" } }, - "node_modules/typescript": { - "version": "5.9.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", - "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", - "dev": true, - "license": "Apache-2.0", - "bin": { - "tsc": "bin/tsc", - "tsserver": "bin/tsserver" - }, - "engines": { - "node": ">=14.17" + "node_modules/zod": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.4.3.tgz", + "integrity": "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" } }, - "node_modules/undici-types": { - "version": "6.21.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", - "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/watskeburt": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/watskeburt/-/watskeburt-4.2.3.tgz", - "integrity": "sha512-uG9qtQYoHqAsnT711nG5iZc/8M5inSmkGCOp7pFaytKG2aTfIca7p//CjiVzAE4P7hzaYuCozMjNNaLgmhbK5g==", - "dev": true, - "license": "MIT", - "bin": { - "watskeburt": "dist/run-cli.js" - }, - "engines": { - "node": "^18||>=20" + "node_modules/zod-to-json-schema": { + "version": "3.25.2", + "resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.25.2.tgz", + "integrity": "sha512-O/PgfnpT1xKSDeQYSCfRI5Gy3hPf91mKVDuYLUHZJMiDFptvP41MSnWofm8dnCm0256ZNfZIM7DSzuSMAFnjHA==", + "license": "ISC", + "peerDependencies": { + "zod": "^3.25.28 || ^4" } } } diff --git a/demo/ai-todo-assistant/package.json b/demo/ai-todo-assistant/package.json index 80587c0..802610a 100644 --- a/demo/ai-todo-assistant/package.json +++ b/demo/ai-todo-assistant/package.json @@ -4,19 +4,53 @@ "private": true, "type": "module", "scripts": { + "dev": "concurrently \"npm:dev:api\" \"npm:dev:web\"", + "dev:api": "tsx watch core/backend/server.ts", + "dev:web": "vite --host 127.0.0.1", + "mcp": "tsx core/backend/mcp.ts", + "outbox": "tsx core/backend/outbox/worker.ts", + "start": "node dist/server/core/backend/server.js", + "start:mcp": "node dist/server/core/backend/mcp.js", + "start:outbox": "node dist/server/core/backend/outbox/worker.js", + "build": "npm run generate:contracts && tsc --noEmit && npm run build:server && vite build && npm run check:bundle", + "build:server": "tsc -p tsconfig.server.json && node core/architecture/copy-server-assets.mjs", "validate:architecture": "tsx core/architecture/validate-feature-definitions.ts", "generate:contracts": "tsx core/architecture/generate-contract-types.ts", - "check:imports": "depcruise --config core/architecture/.dependency-cruiser.cjs .", + "check:imports": "depcruise --config core/architecture/.dependency-cruiser.cjs core contexts composition tests", + "test": "tsx --test tests/*.test.ts tests/*.test.tsx", + "test:integration": "tsx --test tests/mongo.integration.test.ts", + "test:smoke": "node tests/built-server.smoke.mjs", + "check:bundle": "node core/architecture/check-bundle-size.mjs", "check": "npm run validate:architecture && npm run generate:contracts && npm run check:imports && tsc --noEmit" }, "devDependencies": { "@types/node": "^22.10.2", + "@types/react": "^19.2.18", + "@types/react-dom": "^19.2.4", "ajv": "^8.17.1", "ajv-formats": "^3.0.1", + "concurrently": "^9.1.2", "dependency-cruiser": "^16.10.0", "json-schema-to-typescript": "^15.0.4", + "mongodb-memory-server": "^11.2.0", "tsx": "^4.19.2", "typescript": "^5.7.2" }, - "engines": { "node": ">=20" } + "dependencies": { + "@emotion/react": "^11.14.0", + "@emotion/styled": "^11.14.0", + "@modelcontextprotocol/sdk": "^1.12.0", + "@mui/icons-material": "^7.0.2", + "@mui/material": "^7.0.2", + "dotenv": "^16.4.7", + "jose": "^6.2.9", + "mongodb": "^6.12.0", + "react": "^19.0.0", + "react-dom": "^19.0.0", + "vite": "^6.0.7", + "zod": "^4.4.3" + }, + "engines": { + "node": ">=20" + } } diff --git a/demo/ai-todo-assistant/public/favicon.svg b/demo/ai-todo-assistant/public/favicon.svg new file mode 100644 index 0000000..e0db426 --- /dev/null +++ b/demo/ai-todo-assistant/public/favicon.svg @@ -0,0 +1 @@ + diff --git a/demo/ai-todo-assistant/public/readme-preview.jpg b/demo/ai-todo-assistant/public/readme-preview.jpg new file mode 100644 index 0000000000000000000000000000000000000000..6cac5e43a51c032b8c581b513e69462168eddbd9 GIT binary patch literal 71347 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 literal 0 HcmV?d00001 diff --git a/demo/ai-todo-assistant/tests/ai-provider.test.ts b/demo/ai-todo-assistant/tests/ai-provider.test.ts new file mode 100644 index 0000000..5410b6e --- /dev/null +++ b/demo/ai-todo-assistant/tests/ai-provider.test.ts @@ -0,0 +1,79 @@ +import assert from "node:assert/strict"; +import test from "node:test"; +import { createOllamaTaskPlanner } from "../contexts/ai-planning/infrastructure/ollama-task-planner.js"; +import { TaskCategory, TaskPriority } from "../contexts/task-management/contracts/v1/task-taxonomy.js"; + +const input = { goal: "Ship safely", category: TaskCategory.Work, priority: TaskPriority.High }; +const validSuggestions = [ + { + title: "Review release checklist", + category: TaskCategory.Work, + priority: TaskPriority.High, + rationale: "Catch issues before release", + }, +]; +const success = () => + new Response(JSON.stringify({ response: JSON.stringify(validSuggestions) }), { status: 200 }); + +test("Ollama planner retries transient failures and records successful provenance", async () => { + let calls = 0; + const planner = createOllamaTaskPlanner("http://ollama.test", "test-model", { + fetch: async () => (++calls === 1 ? new Response("unavailable", { status: 503 }) : success()), + }); + const result = await planner(input); + assert.equal(result.ok, true); + assert.equal(calls, 2); + if (result.ok) assert.equal(result.provenance.model, "test-model"); + assert.deepEqual(planner.health(), { + requests: 1, + successes: 1, + failures: 0, + rejected: 0, + active: 0, + lastLatencyMs: planner.health().lastLatencyMs, + }); +}); + +test("Ollama planner rejects invalid contract output and opens its circuit", async () => { + let calls = 0; + const planner = createOllamaTaskPlanner("http://ollama.test", "test-model", { + now: () => 1_000, + fetch: async () => { + calls++; + return new Response(JSON.stringify({ response: JSON.stringify([{ title: "Incomplete" }]) })); + }, + }); + for (let attempt = 0; attempt < 3; attempt++) { + const result = await planner(input); + assert.equal(result.ok, false); + if (!result.ok) assert.equal(result.code, "AssistantResponseInvalid"); + } + const rejected = await planner(input); + assert.equal(rejected.ok, false); + if (!rejected.ok) assert.match(rejected.message, /circuit is open/); + assert.equal(calls, 3); + assert.equal(planner.health().rejected, 1); + assert.equal(planner.health().active, 0); +}); + +test("Ollama planner caps concurrent and per-minute requests", async () => { + const resolvers: Array<(response: Response) => void> = []; + const concurrentPlanner = createOllamaTaskPlanner("http://ollama.test", "test-model", { + fetch: () => new Promise((resolve) => resolvers.push(resolve)), + }); + const active = Array.from({ length: 4 }, () => concurrentPlanner(input)); + const overflow = await concurrentPlanner(input); + assert.equal(overflow.ok, false); + resolvers.forEach((resolve) => resolve(success())); + await Promise.all(active); + assert.equal(concurrentPlanner.health().rejected, 1); + assert.equal(concurrentPlanner.health().active, 0); + + const ratePlanner = createOllamaTaskPlanner("http://ollama.test", "test-model", { + now: () => 5_000, + fetch: async () => success(), + }); + for (let request = 0; request < 30; request++) assert.equal((await ratePlanner(input)).ok, true); + assert.equal((await ratePlanner(input)).ok, false); + assert.equal(ratePlanner.health().rejected, 1); +}); diff --git a/demo/ai-todo-assistant/tests/built-server.smoke.mjs b/demo/ai-todo-assistant/tests/built-server.smoke.mjs new file mode 100644 index 0000000..54ddc40 --- /dev/null +++ b/demo/ai-todo-assistant/tests/built-server.smoke.mjs @@ -0,0 +1,44 @@ +import assert from "node:assert/strict"; +import { spawn } from "node:child_process"; + +const port = 32_000 + Math.floor(Math.random() * 1_000); +const child = spawn(process.execPath, ["dist/server/core/backend/server.js"], { + cwd: process.cwd(), + env: { + ...process.env, + NODE_ENV: "development", + HOST: "127.0.0.1", + PORT: String(port), + PUBLIC_BASE_URL: `http://127.0.0.1:${port}`, + MONGODB_DATABASE: `built_server_smoke_${process.pid}`, + }, + stdio: ["ignore", "pipe", "pipe"], +}); +let output = ""; +child.stdout.on("data", (chunk) => (output += chunk)); +child.stderr.on("data", (chunk) => (output += chunk)); + +try { + const deadline = Date.now() + 45_000; + let response; + while (Date.now() < deadline) { + if (child.exitCode !== null) throw new Error(`Built server exited early (${child.exitCode})\n${output}`); + try { + response = await fetch(`http://127.0.0.1:${port}/health/ready`); + if (response.ok) break; + } catch {} + await new Promise((resolve) => setTimeout(resolve, 250)); + } + assert.equal(response?.status, 200, `Built server did not become ready\n${output}`); + const readiness = await response.json(); + assert.equal(readiness.status, "ready"); + assert.equal(readiness.checks.mongo, "ok"); + console.log("Built server smoke test passed."); +} finally { + child.kill("SIGTERM"); + await Promise.race([ + new Promise((resolve) => child.once("exit", resolve)), + new Promise((resolve) => setTimeout(resolve, 10_000)), + ]); + if (child.exitCode === null) child.kill("SIGKILL"); +} diff --git a/demo/ai-todo-assistant/tests/core-quality.test.ts b/demo/ai-todo-assistant/tests/core-quality.test.ts new file mode 100644 index 0000000..bc611ae --- /dev/null +++ b/demo/ai-todo-assistant/tests/core-quality.test.ts @@ -0,0 +1,171 @@ +import assert from "node:assert/strict"; +import { mkdtemp, mkdir, writeFile } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import test from "node:test"; +import { discoverFeatureManifests } from "../core/backend/discovery/discover-features.js"; +import { createCanonicalFingerprint } from "../core/backend/idempotency/canonical-fingerprint.js"; +import { matchRoute } from "../core/backend/http/route-matcher.js"; +import { + HttpMethod, + PermissionScope, + SlotName, +} from "../core/shared/architecture-enums.js"; +import { FeatureName } from "../core/shared/generated/feature-names.js"; +import type { FeatureManifest } from "../core/shared/feature-manifest.js"; +import { Client } from "@modelcontextprotocol/sdk/client/index.js"; +import { InMemoryTransport } from "@modelcontextprotocol/sdk/inMemory.js"; +import { createFeatureMcpServer } from "../core/backend/mcp-server.js"; + +const manifest: FeatureManifest = { + name: FeatureName.ChangeTaskStatus, + backend: { + method: HttpMethod.Patch, + route: "/api/tasks/:taskId/status", + requiredScopes: [PermissionScope.TaskWrite], + feature: { + name: FeatureName.ChangeTaskStatus, + capabilities: [], + execute: async () => ({ ok: true, value: {} }), + }, + }, + frontend: { contributions: [{ slot: SlotName.TaskRowActions, name: "test" }] }, +}; + +test("canonical idempotency fingerprints ignore JSON property order", () => { + assert.equal( + createCanonicalFingerprint({ input: { title: "Plan", priority: "High" }, taskId: "task-1" }), + createCanonicalFingerprint({ taskId: "task-1", input: { priority: "High", title: "Plan" } }), + ); + assert.notEqual( + createCanonicalFingerprint({ values: ["first", "second"] }), + createCanonicalFingerprint({ values: ["second", "first"] }), + ); +}); + +test("route matcher returns typed path parameters for a discovered manifest", () => { + const matched = matchRoute([manifest], HttpMethod.Patch, "/api/tasks/task%201/status"); + assert.equal(matched?.manifest.name, FeatureName.ChangeTaskStatus); + assert.deepEqual(matched?.params, { taskId: "task 1" }); + assert.equal(matchRoute([manifest], HttpMethod.Get, "/api/tasks/task-1/status"), null); +}); + +test("dynamic discovery rejects an invalid manifest before route registration", async () => { + const root = await mkdtemp(join(tmpdir(), "aifa-invalid-manifest-")); + await mkdir(join(root, "feature")); + await mkdir(join(root, "feature", "contracts")); + await writeFile( + join(root, "feature", "feature.definition.json"), + JSON.stringify({ + name: "create-task", + businessNeed: "test", + backend: { + route: "/api/tasks", + method: "POST", + capabilities: [], + mcp: { exposed: false }, + }, + frontend: { slots: [] }, + contracts: { input: "contracts/input.schema.json", output: "contracts/output.schema.json" }, + events: [], + security: { mcpScopes: [], requiresConfirmation: false }, + }), + ); + await writeFile( + join(root, "feature", "contracts", "input.schema.json"), + JSON.stringify({ type: "object" }), + ); + await writeFile( + join(root, "feature", "contracts", "output.schema.json"), + JSON.stringify({ type: "object" }), + ); + await writeFile(join(root, "feature", "manifest.ts"), "export const manifest = {};\n"); + await assert.rejects(discoverFeatureManifests(root), /Invalid feature manifest/); +}); + +test("discovery compiles executable input and output contracts", async () => { + const manifests = await discoverFeatureManifests(join(process.cwd(), "contexts")); + const createTask = manifests.find(({ name }) => name === FeatureName.CreateTask); + assert.ok(createTask?.backend.contract); + assert.equal( + createTask.backend.contract.validateInput({ + title: "Valid", + category: "Work", + priority: "High", + }).length, + 0, + ); + assert.ok( + createTask.backend.contract.validateInput({ + title: "Valid", + category: "not-a-category", + priority: "High", + }).length > 0, + ); + assert.ok( + createTask.backend.contract.validateOutput({ + task: { + id: "task", + tenantId: "must-not-leak", + title: "Valid", + category: "Work", + priority: "High", + status: "Todo", + version: 1, + createdAt: "2026-01-01T00:00:00.000Z", + updatedAt: "2026-01-01T00:00:00.000Z", + completedAt: null, + }, + }).length > 0, + ); +}); + +test("MCP projects the HTTP contract, route, identity, and idempotency metadata", async () => { + let forwarded: { url: string; init?: RequestInit } | undefined; + const mcpManifest: FeatureManifest = { + ...manifest, + backend: { + ...manifest.backend, + contract: { + inputSchema: { + type: "object", + additionalProperties: false, + required: ["taskId", "status"], + properties: { taskId: { type: "string" }, status: { type: "string" } }, + }, + outputSchema: { type: "object" }, + validateInput: () => [], + validateOutput: () => [], + validateEvent: () => [], + }, + }, + mcp: { toolName: "change_task_status", description: "Change status" }, + }; + const server = createFeatureMcpServer( + [mcpManifest], + { authorization: "Bearer actor-token" }, + "http://api.test", + async (url, init) => { + forwarded = { url: String(url), init }; + return new Response(JSON.stringify({ ok: true, value: { changed: true } })); + }, + ); + const client = new Client({ name: "test-client", version: "1.0.0" }); + const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair(); + await server.connect(serverTransport); + await client.connect(clientTransport); + try { + const result = await client.callTool({ + name: "change_task_status", + arguments: { taskId: "task/one", status: "Completed", commandId: "command-1234567890" }, + }); + assert.equal(result.isError, false); + assert.equal(forwarded?.url, "http://api.test/api/tasks/task%2Fone/status"); + assert.equal(new Headers(forwarded?.init?.headers).get("authorization"), "Bearer actor-token"); + assert.equal(new Headers(forwarded?.init?.headers).get("idempotency-key"), "command-1234567890"); + assert.deepEqual(JSON.parse(String(forwarded?.init?.body)), { status: "Completed" }); + } finally { + await client.close(); + await server.close(); + } +}); diff --git a/demo/ai-todo-assistant/tests/features.test.ts b/demo/ai-todo-assistant/tests/features.test.ts new file mode 100644 index 0000000..ff76797 --- /dev/null +++ b/demo/ai-todo-assistant/tests/features.test.ts @@ -0,0 +1,213 @@ +import assert from "node:assert/strict"; +import test from "node:test"; +import { createAifaRuntime } from "../core/backend/runtime/aifa-runtime.js"; +import { DomainEventType } from "../core/shared/architecture-enums.js"; +import { FeatureName } from "../core/shared/generated/feature-names.js"; +import { + TaskCategory, + TaskPriority, + TaskStatus, + type Task, +} from "../contexts/task-management/domain/task.js"; +import { feature as createTask } from "../contexts/task-management/features/create-task/backend/feature.js"; +import { feature as changeStatus } from "../contexts/task-management/features/change-task-status/backend/feature.js"; +import { feature as deleteTask } from "../contexts/task-management/features/delete-task/backend/feature.js"; +import { feature as listTasks } from "../contexts/task-management/features/list-tasks/backend/feature.js"; +import { feature as generatePlan } from "../contexts/ai-planning/features/generate-task-plan/backend/feature.js"; +import { resolveActor } from "../core/backend/auth/actor-resolver.js"; +import { publishPendingEvents } from "../core/backend/outbox/outbox-publisher.js"; + +function fixture() { + const actor = { tenantId: "tenant-1", userId: "user-1", scopes: [] }; + const tasks = new Map(); + const plans: unknown[] = []; + let id = 0; + const audit: string[] = []; + const runtime = createAifaRuntime({ + TaskCreate: async ({ task }: { task: Omit }) => { + const stored = { ...task, tenantId: actor.tenantId }; + tasks.set(task.id, stored); + return stored; + }, + TaskList: async () => ({ tasks: [...tasks.values()] }), + TaskLoad: async ({ taskId }: { taskId: string }) => tasks.get(taskId) ?? null, + TaskSave: async ({ taskId, changes, expectedVersion }: { taskId: string; changes: Partial; expectedVersion: number }) => { + const current = tasks.get(taskId); + if (!current || current.version !== expectedVersion) return null; + const next = { ...current, ...changes }; + tasks.set(taskId, next); + return next; + }, + TaskDelete: async ({ taskId, expectedVersion }: { taskId: string; expectedVersion: number }) => + tasks.get(taskId)?.version === expectedVersion ? tasks.delete(taskId) : false, + IdCreate: async () => `task-${++id}`, + ClockNow: async () => "2026-01-01T00:00:00.000Z", + DomainEventEmit: async ({ eventType }: { eventType: DomainEventType }) => { + audit.push(eventType); + }, + TaskPlanCreate: async ({ plan }: { plan: unknown }) => { + plans.push(plan); + return plan; + }, + AssistantGenerateTaskPlan: async () => ({ + ok: true as const, + suggestions: [ + { + title: "First step", + category: TaskCategory.Work, + priority: TaskPriority.High, + rationale: "Start small", + }, + ], + provenance: { + provider: "fixture", + model: "test", + promptVersion: "v1", + latencyMs: 1, + }, + }), + }); + const metadata = { + correlationId: "correlation", + causationId: "cause", + idempotencyKey: "1234567890123456", + }; + return { runtime, actor, metadata, audit, plans }; +} +test("task features create, list, transition, and delete within the tenant", async () => { + const { runtime, actor, metadata, audit } = fixture(); + const created = await runtime.run( + createTask, + { title: " Ship AIFA ", category: TaskCategory.Work, priority: TaskPriority.High }, + actor, + metadata, + ); + assert.equal(created.ok, true); + if (!created.ok) return; + assert.equal(created.value.task.title, "Ship AIFA"); + const listed = await runtime.run(listTasks, {}, actor, metadata); + assert.equal(listed.ok, true); + if (!listed.ok) return; + assert.equal(listed.value.tasks.length, 1); + const changed = await runtime.run( + changeStatus, + { taskId: created.value.task.id, status: TaskStatus.Completed, expectedVersion: 1 }, + actor, + metadata, + ); + assert.equal(changed.ok, true); + if (!changed.ok) return; + assert.equal(changed.value.task.completedAt, "2026-01-01T00:00:00.000Z"); + const removed = await runtime.run( + deleteTask, + { taskId: created.value.task.id, expectedVersion: 2, confirmed: true }, + actor, + metadata, + ); + assert.equal(removed.ok, true); + assert.deepEqual(audit, ["TaskCreatedV1", "TaskStatusChangedV1", "TaskDeletedV1"]); +}); +test("features return explicit failures for invalid input and stale writes", async () => { + const { runtime, actor, metadata } = fixture(); + const invalid = await runtime.run( + createTask, + { title: " ", category: TaskCategory.Work, priority: TaskPriority.Low }, + actor, + metadata, + ); + assert.equal(invalid.ok, false); + if (!invalid.ok) assert.equal(invalid.error.code, "InvalidInput"); + const stale = await runtime.run( + changeStatus, + { taskId: "missing", status: TaskStatus.Completed, expectedVersion: 1 }, + actor, + metadata, + ); + assert.equal(stale.ok, false); + if (!stale.ok) assert.equal(stale.error.code, "NotFound"); +}); +test("AI planning stays provider-neutral and audits each plan", async () => { + const { runtime, actor, metadata, audit, plans } = fixture(); + const result = await runtime.run( + generatePlan, + { + goal: "Prepare launch", + category: TaskCategory.Work, + priority: TaskPriority.High, + }, + actor, + metadata, + ); + assert.equal(result.ok, true); + if (!result.ok) return; + assert.equal(result.value.suggestions[0]?.title, "First step"); + assert.equal(plans.length, 1); + assert.deepEqual(audit, ["TaskPlanGeneratedV1"]); + assert.equal(FeatureName.GenerateTaskPlan, generatePlan.name); +}); +test("actor resolution accepts only configured scope values", async () => { + assert.deepEqual( + await resolveActor({ + "x-aifa-tenant-id": "tenant", + "x-aifa-user-id": "user", + "x-aifa-scopes": "TaskRead,not-real", + }), + { tenantId: "tenant", userId: "user", scopes: ["TaskRead"] }, + ); +}); +test("runtime rejects missing and undeclared capability access without returning a fake value", async () => { + const actor = { tenantId: "tenant", userId: "user", scopes: [] }; + const metadata = { correlationId: "c", causationId: "c" }; + const missing = await createAifaRuntime({}).run( + { + name: FeatureName.ListTasks, + capabilities: ["TaskList" as never], + execute: async (context) => context.ok({}), + }, + {}, + actor, + metadata, + ); + assert.equal(missing.ok, false); + if (!missing.ok) assert.equal(missing.error.code, "CapabilityMissing"); + + const undeclared = await createAifaRuntime({ ClockNow: async () => "now" }).run( + { + name: FeatureName.ListTasks, + capabilities: [], + execute: async (context) => { + await (context.capabilities as { ClockNow(): Promise }).ClockNow(); + return context.ok({}); + }, + }, + {}, + actor, + metadata, + ); + assert.equal(undeclared.ok, false); + if (!undeclared.ok) assert.equal(undeclared.error.code, "CapabilityNotAllowed"); +}); +test("outbox delivery marks successes and leaves failures retryable", async () => { + const delivered: unknown[] = []; + const failed: unknown[] = []; + const count = await publishPendingEvents( + { + claim: async () => [ + { _id: 1, claimToken: "one", eventId: "e1", eventType: "TaskCreatedV1", tenantId: "a", occurredAt: "now", attempts: 1 }, + { _id: 2, claimToken: "two", eventId: "e2", eventType: "TaskDeletedV1", tenantId: "a", occurredAt: "now", attempts: 1 }, + ], + markDelivered: async (id) => { + delivered.push(id); + }, + markFailed: async (id) => { + failed.push(id); + }, + }, + async (event) => { + if (event._id === 2) throw new Error("downstream unavailable"); + }, + ); + assert.equal(count, 2); + assert.deepEqual(delivered, [1]); + assert.deepEqual(failed, [2]); +}); diff --git a/demo/ai-todo-assistant/tests/frontend-accessibility.test.tsx b/demo/ai-todo-assistant/tests/frontend-accessibility.test.tsx new file mode 100644 index 0000000..f21c3b7 --- /dev/null +++ b/demo/ai-todo-assistant/tests/frontend-accessibility.test.tsx @@ -0,0 +1,42 @@ +import assert from "node:assert/strict"; +import test from "node:test"; +import React, { type ReactNode } from "react"; +import { renderToStaticMarkup } from "react-dom/server"; +import { QueryClient } from "../core/frontend/query/query-client.js"; +import { QueryClientProvider } from "../core/frontend/query/react-query.js"; +import { contribution as createTask } from "../contexts/task-management/features/create-task/frontend/contribution.js"; +import { contribution as deleteTask } from "../contexts/task-management/features/delete-task/frontend/contribution.js"; +import { TaskCategory, TaskPriority, TaskStatus } from "../contexts/task-management/contracts/v1/task-taxonomy.js"; + +function withFrontendRuntime(children: ReactNode) { + return {children}; +} + +test("task composer exposes names for every interactive field", () => { + const markup = renderToStaticMarkup(withFrontendRuntime(createTask.render({}))); + assert.match(markup, /aria-label="Task title"/); + assert.match(markup, /aria-label="Task category"/); + assert.match(markup, /aria-label="Task priority"/); + assert.match(markup, />Add task { + const markup = renderToStaticMarkup( + withFrontendRuntime( + deleteTask.render({ + task: { + id: "task-1", + title: "Release checklist", + category: TaskCategory.Work, + priority: TaskPriority.High, + status: TaskStatus.Todo, + version: 1, + createdAt: "2026-01-01T00:00:00.000Z", + updatedAt: "2026-01-01T00:00:00.000Z", + completedAt: null, + }, + }), + ), + ); + assert.match(markup, /aria-label="delete Release checklist"/); +}); diff --git a/demo/ai-todo-assistant/tests/frontend-runtime.test.ts b/demo/ai-todo-assistant/tests/frontend-runtime.test.ts new file mode 100644 index 0000000..a62a70f --- /dev/null +++ b/demo/ai-todo-assistant/tests/frontend-runtime.test.ts @@ -0,0 +1,95 @@ +import assert from "node:assert/strict"; +import test from "node:test"; +import { eventConsumers as createTaskEventConsumers } from "../contexts/task-management/features/create-task/frontend/contribution.js"; +import { TaskCacheTag, TaskQueryName } from "../contexts/task-management/contracts/v1/task-cache.js"; +import { DomainEventType } from "../core/shared/architecture-enums.js"; +import { FrontendEventRegistry } from "../core/frontend/events/event-registry.js"; +import { QueryClient } from "../core/frontend/query/query-client.js"; + +async function settle(): Promise { + await new Promise((resolve) => setTimeout(resolve, 0)); +} + +test("semantic tag invalidation refetches every active dependent query", async () => { + const client = new QueryClient(); + let listRequests = 0; + let countRequests = 0; + const listHash = client.configure({ + key: [TaskQueryName.List, "", ""], + tags: [TaskCacheTag.Collection], + queryFn: async () => ++listRequests, + }); + const countHash = client.configure({ + key: ["task-management/v1/task-count"], + tags: [TaskCacheTag.Collection], + queryFn: async () => ++countRequests, + }); + const unsubscribeList = client.subscribe(listHash, () => undefined); + const unsubscribeCount = client.subscribe(countHash, () => undefined); + await settle(); + + assert.equal(client.getSnapshot(listHash).data, 1); + assert.equal(client.getSnapshot(countHash).data, 1); + + await Promise.all([ + client.invalidateTags([TaskCacheTag.Collection]), + client.invalidateTags([TaskCacheTag.Collection]), + ]); + + assert.equal(listRequests, 2, "duplicate invalidations deduplicate the active list request"); + assert.equal(countRequests, 2, "count updates without a component-to-component call"); + assert.equal(client.getSnapshot(listHash).data, 2); + assert.equal(client.getSnapshot(countHash).data, 2); + unsubscribeList(); + unsubscribeCount(); +}); + +test("TaskCreatedV1 consumer invalidates the task collection contract", async () => { + const client = new QueryClient(); + const registry = new FrontendEventRegistry(); + createTaskEventConsumers.forEach((consumer) => registry.register(consumer)); + let requests = 0; + const hash = client.configure({ + key: [TaskQueryName.List, "", ""], + tags: [TaskCacheTag.Collection], + queryFn: async () => ++requests, + }); + const unsubscribe = client.subscribe(hash, () => undefined); + await settle(); + + await registry.dispatch( + { + eventId: "event-1", + eventType: DomainEventType.TaskCreatedV1, + schemaVersion: 1, + tenantId: "tenant-1", + actorId: "actor-1", + correlationId: "correlation-1", + causationId: "causation-1", + idempotencyKey: "idempotency-1", + taskId: "task-1", + occurredAt: "2026-08-15T12:00:00.000Z", + }, + { queryClient: client }, + ); + + assert.equal(requests, 2); + assert.equal(client.getSnapshot(hash).data, 2); + unsubscribe(); +}); + +test("frontend event consumers reject unsupported contract versions", async () => { + const registry = new FrontendEventRegistry(); + createTaskEventConsumers.forEach((consumer) => registry.register(consumer)); + await assert.rejects( + registry.dispatch( + { + eventType: DomainEventType.TaskCreatedV1, + schemaVersion: 2, + tenantId: "tenant-1", + }, + { queryClient: new QueryClient() }, + ), + /failed consumer validation/, + ); +}); diff --git a/demo/ai-todo-assistant/tests/http-boundary.test.ts b/demo/ai-todo-assistant/tests/http-boundary.test.ts new file mode 100644 index 0000000..27fe6ec --- /dev/null +++ b/demo/ai-todo-assistant/tests/http-boundary.test.ts @@ -0,0 +1,93 @@ +import assert from "node:assert/strict"; +import { createServer } from "node:http"; +import test from "node:test"; +import { createRequestHandler } from "../core/backend/http/request-handler.js"; +import { + HttpMethod, + PermissionScope, + SlotName, +} from "../core/shared/architecture-enums.js"; +import { FeatureName } from "../core/shared/generated/feature-names.js"; +test("HTTP boundary enforces actor and idempotency before invoking features", async () => { + let calls = 0; + const handler = createRequestHandler({ + manifests: [ + { + name: FeatureName.CreateTask, + backend: { + method: HttpMethod.Post, + route: "/api/tasks", + requiredScopes: [PermissionScope.TaskWrite], + contract: { + inputSchema: { type: "object" }, + outputSchema: { type: "object" }, + validateInput: (value) => + (value as Record).title + ? [] + : [{ path: "/title", message: "is required" }], + validateOutput: () => [], + validateEvent: () => [], + }, + feature: { + name: FeatureName.CreateTask, + capabilities: [], + execute: async () => ({ ok: true, value: {} }), + }, + }, + frontend: { contributions: [{ slot: SlotName.TaskComposer, name: "test" }] }, + }, + ], + resolveActor: (headers) => + headers["x-aifa-user-id"] + ? { + tenantId: "t", + userId: "u", + scopes: headers["x-aifa-scopes"]?.toString().split(",") ?? [], + } + : null, + run: async () => { + calls++; + return { ok: true, value: { accepted: true } }; + }, + }); + const server = createServer(handler); + await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve)); + const address = server.address(); + if (!address || typeof address === "string") throw new Error("No port"); + try { + const unauthenticated = await fetch(`http://127.0.0.1:${address.port}/api/tasks`, { + method: "POST", + headers: { "content-type": "application/json" }, + body: "{}", + }); + assert.equal(unauthenticated.status, 401); + const forbidden = await fetch(`http://127.0.0.1:${address.port}/api/tasks`, { + method: "POST", + headers: { "content-type": "application/json", "x-aifa-user-id": "u", "idempotency-key": "1234567890123456" }, + body: "{}", + }); + assert.equal(forbidden.status, 403); + const invalid = await fetch(`http://127.0.0.1:${address.port}/api/tasks`, { + method: "POST", + headers: { "content-type": "application/json", "x-aifa-user-id": "u", "x-aifa-scopes": "TaskWrite" }, + body: "{}", + }); + assert.equal(invalid.status, 400); + const accepted = await fetch(`http://127.0.0.1:${address.port}/api/tasks`, { + method: "POST", + headers: { + "x-aifa-user-id": "u", + "x-aifa-scopes": "TaskWrite", + "idempotency-key": "1234567890123456", + "content-type": "application/json", + }, + body: JSON.stringify({ title: "Valid task" }), + }); + assert.equal(accepted.status, 200); + assert.equal(calls, 1); + } finally { + await new Promise((resolve, reject) => + server.close((error) => (error ? reject(error) : resolve())), + ); + } +}); diff --git a/demo/ai-todo-assistant/tests/mongo.integration.test.ts b/demo/ai-todo-assistant/tests/mongo.integration.test.ts new file mode 100644 index 0000000..3fd8b34 --- /dev/null +++ b/demo/ai-todo-assistant/tests/mongo.integration.test.ts @@ -0,0 +1,238 @@ +import assert from "node:assert/strict"; +import { randomUUID } from "node:crypto"; +import test from "node:test"; +import { MongoClient } from "mongodb"; +import { MongoMemoryReplSet } from "mongodb-memory-server"; +import { createMongoIdempotencyStore } from "../core/backend/idempotency/mongo-idempotency.js"; +import { FeatureName } from "../core/shared/generated/feature-names.js"; +import { DomainEventType, OutboxStatus } from "../core/shared/architecture-enums.js"; +import { createMongoOutboxStore } from "../core/backend/outbox/mongo-outbox.js"; +import { createMongoPlatformCapabilities } from "../core/backend/runtime/mongo-platform-capabilities.js"; +import { discoverFeatureManifests } from "../core/backend/discovery/discover-features.js"; +import { + createMongoTaskCapabilities, + ensureTaskManagementIndexes, +} from "../contexts/task-management/infrastructure/mongo-task-capabilities.js"; +import { + TaskCategory, + TaskPriority, + TaskStatus, + type Task, +} from "../contexts/task-management/domain/task.js"; +test("Mongo task capability enforces tenant isolation and optimistic concurrency", async () => { + const memoryServer = process.env.MONGODB_URI + ? undefined + : await MongoMemoryReplSet.create({ replSet: { count: 1 } }); + const client = new MongoClient(process.env.MONGODB_URI ?? memoryServer!.getUri()); + const database = client.db(`ai_todo_test_${randomUUID().replaceAll("-", "")}`); + try { + await client.connect(); + await ensureTaskManagementIndexes(database); + const actorA = { tenantId: "tenant-a", userId: "user-a", scopes: [] }; + const actorB = { tenantId: "tenant-b", userId: "user-b", scopes: [] }; + const capabilities = createMongoTaskCapabilities(database, actorA); + const task: Task = { + id: "task-1", + tenantId: "tenant-a", + title: "Integration task", + category: TaskCategory.Work, + priority: TaskPriority.High, + status: TaskStatus.Todo, + version: 1, + createdAt: "2026-01-01T00:00:00.000Z", + updatedAt: "2026-01-01T00:00:00.000Z", + completedAt: null, + }; + const { tenantId: _tenantId, ...taskCommand } = task; + await capabilities.TaskCreate({ task: taskCommand }); + assert.equal((await capabilities.TaskList({ limit: 100 })).tasks.length, 1); + assert.equal( + (await createMongoTaskCapabilities(database, actorB).TaskList({ limit: 100 })).tasks.length, + 0, + ); + assert.equal( + await capabilities.TaskSave({ + taskId: task.id, + changes: { + version: 2, + status: TaskStatus.Completed, + updatedAt: task.updatedAt, + completedAt: task.updatedAt, + }, + expectedVersion: 0, + }), + null, + ); + assert.equal( + ( + await capabilities.TaskSave({ + taskId: task.id, + changes: { + version: 2, + status: TaskStatus.Completed, + updatedAt: task.updatedAt, + completedAt: task.updatedAt, + }, + expectedVersion: 1, + }) + )?.version, + 2, + ); + const malicious = await capabilities.TaskCreate({ + task: { ...taskCommand, id: "tenant-override", tenantId: "tenant-b" } as never, + }); + assert.equal(malicious.tenantId, actorA.tenantId); + assert.equal( + await createMongoTaskCapabilities(database, actorB).TaskLoad({ taskId: malicious.id }), + null, + ); + const rollbackTask = { ...task, id: "rolled-back" }; + const session = client.startSession(); + try { + await assert.rejects( + session.withTransaction(async () => { + const { tenantId: _rollbackTenant, ...rollbackCommand } = rollbackTask; + await createMongoTaskCapabilities(database, actorA, session).TaskCreate({ task: rollbackCommand }); + throw new Error("simulate audit failure"); + }), + ); + } finally { + await session.endSession(); + } + assert.equal( + await capabilities.TaskLoad({ taskId: rollbackTask.id }), + null, + ); + const idempotency = createMongoIdempotencyStore(database); + await idempotency.ensureIndexes(); + const command = { + tenantId: "tenant-a", + actorId: "user-a", + featureName: FeatureName.CreateTask, + idempotencyKey: "1234567890123456", + inputFingerprint: "same-input", + }; + const claims = await Promise.all([idempotency.claim(command), idempotency.claim(command)]); + assert.deepEqual(claims.map((claim) => claim.kind).sort(), ["claimed", "in-progress"]); + const claimed = claims.find((claim) => claim.kind === "claimed"); + if (!claimed || claimed.kind !== "claimed") throw new Error("Expected an idempotency claim"); + await idempotency.complete(command, claimed.executionId, { + ok: true, + value: { accepted: true }, + }); + const replay = await idempotency.claim(command); + assert.equal(replay.kind, "completed"); + assert.equal( + (await idempotency.claim({ ...command, inputFingerprint: "other-input" })).kind, + "key-reused", + ); + const interrupted = { ...command, idempotencyKey: "abcdefghijklmnop" }; + const firstAttempt = await idempotency.claim(interrupted); + if (firstAttempt.kind !== "claimed") throw new Error("Expected initial claim"); + await database + .collection("idempotency_records") + .updateOne( + { idempotencyKey: interrupted.idempotencyKey }, + { $set: { leaseExpiresAt: new Date(0).toISOString() } }, + ); + const recoveredAttempt = await idempotency.claim(interrupted); + assert.equal(recoveredAttempt.kind, "claimed"); + if (recoveredAttempt.kind !== "claimed") throw new Error("Expected recovered claim"); + await assert.rejects( + idempotency.complete(interrupted, firstAttempt.executionId, { ok: true, value: {} }), + /execution fence/, + ); + assert.equal((await idempotency.claim(interrupted)).kind, "in-progress"); + await idempotency.complete(interrupted, recoveredAttempt.executionId, { ok: true, value: {} }); + assert.equal((await idempotency.claim(interrupted)).kind, "completed"); + + const preparedCommand = { ...command, idempotencyKey: "prepared-command-1234" }; + const preparedAttempt = await idempotency.claim({ + ...preparedCommand, + commandInput: { goal: "Recover this plan" }, + }); + if (preparedAttempt.kind !== "claimed") throw new Error("Expected prepared command claim"); + await idempotency.heartbeat(preparedCommand, preparedAttempt.executionId); + await idempotency.savePreparedCapability( + preparedCommand, + preparedAttempt.executionId, + "AssistantGenerateTaskPlan", + { ok: true, suggestions: [{ title: "Durable provider result" }] }, + ); + await database.collection("idempotency_records").updateOne( + { idempotencyKey: preparedCommand.idempotencyKey }, + { $set: { leaseExpiresAt: new Date(0).toISOString() } }, + ); + const resumedPreparedAttempt = await idempotency.claim(preparedCommand); + assert.equal(resumedPreparedAttempt.kind, "claimed"); + if (resumedPreparedAttempt.kind !== "claimed") throw new Error("Expected recovered prepared claim"); + assert.deepEqual(resumedPreparedAttempt.preparedCapabilities.AssistantGenerateTaskPlan, { + ok: true, + suggestions: [{ title: "Durable provider result" }], + }); + await assert.rejects( + idempotency.savePreparedCapability( + preparedCommand, + preparedAttempt.executionId, + "AssistantGenerateTaskPlan", + { ok: false }, + ), + /execution fence/, + ); + + const eventManifest = (await discoverFeatureManifests( + new URL("../contexts", import.meta.url).pathname, + )).find(({ name }) => name === FeatureName.CreateTask); + assert.ok(eventManifest?.backend.contract); + await createMongoPlatformCapabilities( + database, + actorA, + { + correlationId: "correlation", + causationId: "cause", + idempotencyKey: "event-command-1234", + }, + undefined, + eventManifest.backend.contract.validateEvent, + ).DomainEventEmit({ eventType: DomainEventType.TaskCreatedV1, data: { taskId: task.id } }); + const auditEvent = await database + .collection("audit_events") + .findOne({ eventType: "TaskCreatedV1" }, { projection: { _id: 0 } }); + assert.deepEqual(eventManifest.backend.contract.validateEvent(auditEvent), []); + const storedOutbox = await database.collection("outbox_events").findOne({ "event.eventId": auditEvent?.eventId }); + assert.deepEqual(eventManifest.backend.contract.validateEvent(storedOutbox?.event), []); + + await database.collection("outbox_events").deleteMany({}); + await database.collection("outbox_events").insertOne({ + event: { + eventId: "concurrent-event", + eventType: "TaskCreatedV1", + tenantId: actorA.tenantId, + occurredAt: new Date().toISOString(), + }, + delivery: { + status: OutboxStatus.Pending, + attempts: 0, + availableAt: new Date(0).toISOString(), + }, + }); + const outbox = createMongoOutboxStore(database, 10); + const concurrentClaims = await Promise.all([ + outbox.claim(1, "worker-a"), + outbox.claim(1, "worker-b"), + ]); + assert.equal( + concurrentClaims.flat().filter(({ eventId }) => eventId === "concurrent-event").length, + 1, + ); + await database.collection("outbox_events").updateOne( + { "event.eventId": "concurrent-event" }, + { $set: { "delivery.leaseExpiresAt": new Date(0).toISOString() } }, + ); + assert.equal((await outbox.claim(10, "recovery-worker")).some(({ eventId }) => eventId === "concurrent-event"), true); + } finally { + await database.dropDatabase(); + await client.close(); + await memoryServer?.stop(); + } +}); diff --git a/demo/ai-todo-assistant/tsconfig.json b/demo/ai-todo-assistant/tsconfig.json index 6019fd1..4b84c82 100644 --- a/demo/ai-todo-assistant/tsconfig.json +++ b/demo/ai-todo-assistant/tsconfig.json @@ -7,7 +7,8 @@ "noEmit": true, "skipLibCheck": true, "esModuleInterop": true, - "allowSyntheticDefaultImports": true + "allowSyntheticDefaultImports": true, + "jsx": "react-jsx" }, - "include": ["core/**/*.ts", "contexts/**/*.ts"] + "include": ["core/**/*.ts", "core/**/*.tsx", "contexts/**/*.ts", "contexts/**/*.tsx"] } diff --git a/demo/ai-todo-assistant/tsconfig.server.json b/demo/ai-todo-assistant/tsconfig.server.json new file mode 100644 index 0000000..a962191 --- /dev/null +++ b/demo/ai-todo-assistant/tsconfig.server.json @@ -0,0 +1,11 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "noEmit": false, + "outDir": "dist/server", + "rootDir": ".", + "sourceMap": true + }, + "include": ["core/**/*.ts", "core/**/*.tsx", "contexts/**/*.ts", "contexts/**/*.tsx"], + "exclude": ["core/frontend/main.tsx", "vite.config.ts"] +} diff --git a/demo/ai-todo-assistant/vite.config.ts b/demo/ai-todo-assistant/vite.config.ts new file mode 100644 index 0000000..703abea --- /dev/null +++ b/demo/ai-todo-assistant/vite.config.ts @@ -0,0 +1,11 @@ +import { defineConfig } from "vite"; + +export default defineConfig({ + server: { + proxy: { + "/api": "http://127.0.0.1:3000", + "/health": "http://127.0.0.1:3000", + }, + }, + build: { outDir: "dist/web", chunkSizeWarningLimit: 450 }, +}); diff --git a/docs/index.html b/docs/index.html index 1f0f07d..0fb4e2f 100644 --- a/docs/index.html +++ b/docs/index.html @@ -56,7 +56,8 @@

      Architecture for agentic coding

      AI Atomic Feature Architecture

      - Build software as small, isolated, AI-implementable features. Each feature ships with a contract: input, output, capabilities, rules, and behavioral tests. + Build software as small, isolated, AI-implementable features. Each feature ships with a + contract: input, output, capabilities, rules, and behavioral tests.

      Atomic tasks @@ -64,7 +65,9 @@

      AI Atomic Feature Architecture

      Runtime capabilities
      @@ -77,7 +80,8 @@

      AI Atomic Feature Architecture

      Can an AI agent implement one feature without understanding the whole project?

      - AIFA moves hidden project knowledge into explicit architectural contracts. The task stays small because the context is packaged, bounded, and testable. + AIFA moves hidden project knowledge into explicit architectural contracts. The task + stays small because the context is packaged, bounded, and testable.

      @@ -92,22 +96,33 @@

      One feature. One context. Narrow capabilities.

      01

      Ticket Contract

      -

      The work item defines the goal, inputs, outputs, rules, failure cases, and acceptance tests.

      +

      + The work item defines the goal, inputs, outputs, rules, failure cases, and acceptance + tests. +

      02

      Atomic Feature

      -

      The implementation receives one context object and uses only declared capabilities.

      +

      + The implementation receives one context object and uses only declared capabilities. +

      03

      Capability Runtime

      -

      The runtime owns infrastructure like persistence, permissions, queues, logging, and transactions.

      +

      + The runtime owns infrastructure like persistence, permissions, queues, logging, and + transactions. +

      04

      Behavioral Tests

      -

      Tests verify what the feature does, so the implementation can evolve without breaking the contract.

      +

      + Tests verify what the feature does, so the implementation can evolve without breaking + the contract. +

      @@ -118,7 +133,8 @@

      Behavioral Tests

      The shift

      From codebase archaeology to feature contracts.

      - Most systems make agents explore first and implement second. AIFA makes the architecture say what the feature may know and do before implementation begins. + Most systems make agents explore first and implement second. AIFA makes the + architecture say what the feature may know and do before implementation begins.

      @@ -143,7 +159,8 @@

      The ticket becomes an interface for humans and AI agents.

      - A good AIFA ticket removes guesswork. It tells the agent what to build, which capabilities are available, which behavior matters, and when the work is done. + A good AIFA ticket removes guesswork. It tells the agent what to build, which + capabilities are available, which behavior matters, and when the work is done.

      • One responsibility
      • @@ -184,7 +201,8 @@

        The ticket becomes an interface for humans and AI agents.

        Proof of concept

        A tiny runtime that proves the shape.

        - The first repo includes a memory runtime, one atomic feature, one ticket contract, and behavioral tests. It is intentionally small so the architecture stays visible. + The first repo includes a memory runtime, one atomic feature, one ticket contract, and + behavioral tests. It is intentionally small so the architecture stays visible.

      @@ -216,11 +234,16 @@

      Designed for delegatable software work.

      Atomic

      -

      Each feature is the smallest meaningful behavior that can be implemented and tested independently.

      +

      + Each feature is the smallest meaningful behavior that can be implemented and tested + independently. +

      Explicit

      -

      Input, output, capabilities, and failure cases are declared before code is written.

      +

      + Input, output, capabilities, and failure cases are declared before code is written. +

      Bounded

      @@ -238,7 +261,8 @@

      Verifiable

      Shareable description

      Software architecture for AI-native implementation.

      - AI Atomic Feature Architecture is a way to design applications so each feature can be implemented from a small contract instead of full project context. + AI Atomic Feature Architecture is a way to design applications so each feature can be + implemented from a small contract instead of full project context.

      Star the repo
      diff --git a/docs/styles.css b/docs/styles.css index 33c2cce..73dd2b8 100644 --- a/docs/styles.css +++ b/docs/styles.css @@ -26,7 +26,12 @@ body { background: var(--paper); color: var(--ink); font-family: - Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", + Inter, + ui-sans-serif, + system-ui, + -apple-system, + BlinkMacSystemFont, + "Segoe UI", sans-serif; line-height: 1.6; overflow-x: hidden; @@ -116,7 +121,13 @@ nav a:hover { .hero::before { background: - linear-gradient(90deg, rgba(16, 26, 22, 0.98) 0%, rgba(16, 26, 22, 0.9) 42%, rgba(16, 26, 22, 0.16) 67%, transparent 100%), + linear-gradient( + 90deg, + rgba(16, 26, 22, 0.98) 0%, + rgba(16, 26, 22, 0.9) 42%, + rgba(16, 26, 22, 0.16) 67%, + transparent 100% + ), linear-gradient(180deg, transparent 0%, rgba(16, 26, 22, 0.18) 100%); content: ""; inset: 0; @@ -182,8 +193,7 @@ nav a:hover { .system-feature { animation-delay: -1.4s; background: - linear-gradient(135deg, rgba(102, 255, 210, 0.18), transparent), - rgba(8, 18, 15, 0.88); + linear-gradient(135deg, rgba(102, 255, 210, 0.18), transparent), rgba(8, 18, 15, 0.88); border-color: rgba(102, 255, 210, 0.78); color: #ffffff; height: 116px; @@ -418,8 +428,7 @@ h3 { code, pre { - font-family: - "SFMono-Regular", Consolas, "Liberation Mono", Menlo, monospace; + font-family: "SFMono-Regular", Consolas, "Liberation Mono", Menlo, monospace; } .comparison code { diff --git a/examples/nodejs-react-typescript/README.md b/examples/nodejs-react-typescript/README.md index d15aa31..4082be8 100644 --- a/examples/nodejs-react-typescript/README.md +++ b/examples/nodejs-react-typescript/README.md @@ -97,19 +97,19 @@ File-backed storage ## Atomic Backend Features -| Feature | Route | Purpose | -| --- | --- | --- | -| `create-task` | `POST /api/tasks` | Create one task | -| `list-tasks` | `GET /api/tasks` | List tasks | +| Feature | Route | Purpose | +| --------------- | ------------------------------ | ----------------- | +| `create-task` | `POST /api/tasks` | Create one task | +| `list-tasks` | `GET /api/tasks` | List tasks | | `complete-task` | `POST /api/tasks/:id/complete` | Complete one task | -| `reopen-task` | `POST /api/tasks/:id/reopen` | Reopen one task | -| `delete-task` | `DELETE /api/tasks/:id` | Delete one task | +| `reopen-task` | `POST /api/tasks/:id/reopen` | Reopen one task | +| `delete-task` | `DELETE /api/tasks/:id` | Delete one task | ## Frontend Slots -| Slot | Model | Purpose | -| --- | --- | --- | -| `APP_HEADER_ACTIONS` | `{ tasks, refreshTasks }` | Add header-level actions | -| `TASK_SUMMARY_CARDS` | `{ tasks }` | Add dashboard summary cards | -| `TASK_ACTIONS` | `{ task, completeTask, reopenTask, deleteTask }` | Add row-level task actions | -| `EMPTY_STATE_ACTIONS` | `{ createExampleTask }` | Add empty state actions | +| Slot | Model | Purpose | +| --------------------- | ------------------------------------------------ | --------------------------- | +| `APP_HEADER_ACTIONS` | `{ tasks, refreshTasks }` | Add header-level actions | +| `TASK_SUMMARY_CARDS` | `{ tasks }` | Add dashboard summary cards | +| `TASK_ACTIONS` | `{ task, completeTask, reopenTask, deleteTask }` | Add row-level task actions | +| `EMPTY_STATE_ACTIONS` | `{ createExampleTask }` | Add empty state actions | diff --git a/examples/nodejs-react-typescript/backend/features/completeTask.ts b/examples/nodejs-react-typescript/backend/features/completeTask.ts index 34fea50..7efd4be 100644 --- a/examples/nodejs-react-typescript/backend/features/completeTask.ts +++ b/examples/nodejs-react-typescript/backend/features/completeTask.ts @@ -1,15 +1,30 @@ import { defineFeature } from "../runtime/feature"; import type { Task } from "../types"; -interface TaskIdInput { taskId: string; } +interface TaskIdInput { + taskId: string; +} export const completeTask = defineFeature({ - name: "complete-task", description: "Mark one existing task as completed.", input: { taskId: "string" }, output: { task: "Task" }, capabilities: ["clock.now", "task.load", "task.save", "audit.record"], + name: "complete-task", + description: "Mark one existing task as completed.", + input: { taskId: "string" }, + output: { task: "Task" }, + capabilities: ["clock.now", "task.load", "task.save", "audit.record"], async execute(context) { const task = await context.capabilities["task.load"]({ taskId: context.input.taskId }); - if (!task) return context.fail("not_found", "Task was not found", { taskId: context.input.taskId }); + if (!task) + return context.fail("not_found", "Task was not found", { taskId: context.input.taskId }); if (task.completed) return context.ok({ task }); - const completedTask: Task = { ...task, completed: true, completedAt: await context.capabilities["clock.now"]() }; + const completedTask: Task = { + ...task, + completed: true, + completedAt: await context.capabilities["clock.now"](), + }; await context.capabilities["task.save"]({ task: completedTask }); - await context.capabilities["audit.record"]({ type: "task.completed", actorId: context.actor.id, taskId: task.id }); + await context.capabilities["audit.record"]({ + type: "task.completed", + actorId: context.actor.id, + taskId: task.id, + }); return context.ok({ task: completedTask }); }, }); diff --git a/examples/nodejs-react-typescript/backend/features/createTask.ts b/examples/nodejs-react-typescript/backend/features/createTask.ts index 528fa18..ceed0b4 100644 --- a/examples/nodejs-react-typescript/backend/features/createTask.ts +++ b/examples/nodejs-react-typescript/backend/features/createTask.ts @@ -1,18 +1,36 @@ import { defineFeature } from "../runtime/feature"; import type { Priority, Task } from "../types"; -interface CreateTaskInput { title?: string; priority?: string; } +interface CreateTaskInput { + title?: string; + priority?: string; +} export const createTask = defineFeature({ - name: "create-task", description: "Create one task with a non-empty title and optional priority.", - input: { title: "string", priority: "low | medium | high" }, output: { task: "Task" }, + name: "create-task", + description: "Create one task with a non-empty title and optional priority.", + input: { title: "string", priority: "low | medium | high" }, + output: { task: "Task" }, capabilities: ["id.create", "clock.now", "task.create", "audit.record"], async execute(context) { const title = context.input.title?.trim() ?? ""; if (!title) return context.fail("invalid_title", "Task title is required"); - const task: Task = { id: await context.capabilities["id.create"](), title, priority: normalizePriority(context.input.priority), completed: false, createdAt: await context.capabilities["clock.now"](), completedAt: null }; + const task: Task = { + id: await context.capabilities["id.create"](), + title, + priority: normalizePriority(context.input.priority), + completed: false, + createdAt: await context.capabilities["clock.now"](), + completedAt: null, + }; await context.capabilities["task.create"]({ task }); - await context.capabilities["audit.record"]({ type: "task.created", actorId: context.actor.id, taskId: task.id }); + await context.capabilities["audit.record"]({ + type: "task.created", + actorId: context.actor.id, + taskId: task.id, + }); return context.ok({ task }); }, }); -function normalizePriority(priority: string | undefined): Priority { return priority === "low" || priority === "medium" || priority === "high" ? priority : "medium"; } +function normalizePriority(priority: string | undefined): Priority { + return priority === "low" || priority === "medium" || priority === "high" ? priority : "medium"; +} diff --git a/examples/nodejs-react-typescript/backend/features/deleteTask.ts b/examples/nodejs-react-typescript/backend/features/deleteTask.ts index b9b1e96..4b4d982 100644 --- a/examples/nodejs-react-typescript/backend/features/deleteTask.ts +++ b/examples/nodejs-react-typescript/backend/features/deleteTask.ts @@ -1,12 +1,23 @@ import { defineFeature } from "../runtime/feature"; -interface TaskIdInput { taskId: string; } +interface TaskIdInput { + taskId: string; +} export const deleteTask = defineFeature({ - name: "delete-task", description: "Delete one existing task.", input: { taskId: "string" }, output: { deleted: "boolean" }, capabilities: ["task.load", "task.delete", "audit.record"], + name: "delete-task", + description: "Delete one existing task.", + input: { taskId: "string" }, + output: { deleted: "boolean" }, + capabilities: ["task.load", "task.delete", "audit.record"], async execute(context) { const task = await context.capabilities["task.load"]({ taskId: context.input.taskId }); - if (!task) return context.fail("not_found", "Task was not found", { taskId: context.input.taskId }); + if (!task) + return context.fail("not_found", "Task was not found", { taskId: context.input.taskId }); await context.capabilities["task.delete"]({ taskId: task.id }); - await context.capabilities["audit.record"]({ type: "task.deleted", actorId: context.actor.id, taskId: task.id }); + await context.capabilities["audit.record"]({ + type: "task.deleted", + actorId: context.actor.id, + taskId: task.id, + }); return context.ok({ deleted: true }); }, }); diff --git a/examples/nodejs-react-typescript/backend/features/listTasks.ts b/examples/nodejs-react-typescript/backend/features/listTasks.ts index e051b7b..b681e0a 100644 --- a/examples/nodejs-react-typescript/backend/features/listTasks.ts +++ b/examples/nodejs-react-typescript/backend/features/listTasks.ts @@ -1,6 +1,12 @@ import { defineFeature } from "../runtime/feature"; import type { Task } from "../types"; export const listTasks = defineFeature, { tasks: Task[] }>({ - name: "list-tasks", description: "List tasks in creation order.", input: {}, output: { tasks: "Task[]" }, capabilities: ["task.list"], - async execute(context) { return context.ok({ tasks: await context.capabilities["task.list"]() }); }, + name: "list-tasks", + description: "List tasks in creation order.", + input: {}, + output: { tasks: "Task[]" }, + capabilities: ["task.list"], + async execute(context) { + return context.ok({ tasks: await context.capabilities["task.list"]() }); + }, }); diff --git a/examples/nodejs-react-typescript/backend/features/reopenTask.ts b/examples/nodejs-react-typescript/backend/features/reopenTask.ts index e71563f..32d400b 100644 --- a/examples/nodejs-react-typescript/backend/features/reopenTask.ts +++ b/examples/nodejs-react-typescript/backend/features/reopenTask.ts @@ -1,15 +1,26 @@ import { defineFeature } from "../runtime/feature"; import type { Task } from "../types"; -interface TaskIdInput { taskId: string; } +interface TaskIdInput { + taskId: string; +} export const reopenTask = defineFeature({ - name: "reopen-task", description: "Mark one completed task as active again.", input: { taskId: "string" }, output: { task: "Task" }, capabilities: ["task.load", "task.save", "audit.record"], + name: "reopen-task", + description: "Mark one completed task as active again.", + input: { taskId: "string" }, + output: { task: "Task" }, + capabilities: ["task.load", "task.save", "audit.record"], async execute(context) { const task = await context.capabilities["task.load"]({ taskId: context.input.taskId }); - if (!task) return context.fail("not_found", "Task was not found", { taskId: context.input.taskId }); + if (!task) + return context.fail("not_found", "Task was not found", { taskId: context.input.taskId }); if (!task.completed) return context.ok({ task }); const reopenedTask: Task = { ...task, completed: false, completedAt: null }; await context.capabilities["task.save"]({ task: reopenedTask }); - await context.capabilities["audit.record"]({ type: "task.reopened", actorId: context.actor.id, taskId: task.id }); + await context.capabilities["audit.record"]({ + type: "task.reopened", + actorId: context.actor.id, + taskId: task.id, + }); return context.ok({ task: reopenedTask }); }, }); diff --git a/examples/nodejs-react-typescript/backend/runtime/aifaRuntime.ts b/examples/nodejs-react-typescript/backend/runtime/aifaRuntime.ts index b01f0d5..24f80b5 100644 --- a/examples/nodejs-react-typescript/backend/runtime/aifaRuntime.ts +++ b/examples/nodejs-react-typescript/backend/runtime/aifaRuntime.ts @@ -1,10 +1,20 @@ import { fail, ok, type Feature, type FeatureResult } from "./feature"; import type { Actor, AuditEntry, RuntimeCapabilities, TaskStore } from "../types"; -interface IdFactory { next(): string; } -interface RuntimeOptions { taskStore: TaskStore; idFactory?: IdFactory; clock?: () => Date; } +interface IdFactory { + next(): string; +} +interface RuntimeOptions { + taskStore: TaskStore; + idFactory?: IdFactory; + clock?: () => Date; +} -export function createAifaRuntime({ taskStore, idFactory = createIdFactory(), clock = () => new Date() }: RuntimeOptions) { +export function createAifaRuntime({ + taskStore, + idFactory = createIdFactory(), + clock = () => new Date(), +}: RuntimeOptions) { const auditLog: AuditEntry[] = []; const allCapabilities: RuntimeCapabilities = { "id.create": async () => idFactory.next(), @@ -14,7 +24,9 @@ export function createAifaRuntime({ taskStore, idFactory = createIdFactory(), cl "task.save": async ({ task }) => taskStore.save(task), "task.delete": async ({ taskId }) => taskStore.delete(taskId), "task.list": async () => taskStore.list(), - "audit.record": async (entry) => { auditLog.push({ ...entry, recordedAt: clock().toISOString() }); }, + "audit.record": async (entry) => { + auditLog.push({ ...entry, recordedAt: clock().toISOString() }); + }, }; function bindCapabilities(feature: Feature): RuntimeCapabilities { @@ -23,20 +35,43 @@ export function createAifaRuntime({ taskStore, idFactory = createIdFactory(), cl get(target, capabilityName) { if (typeof capabilityName !== "string") return undefined; if (!allowed.has(capabilityName as keyof RuntimeCapabilities)) { - return async () => fail("capability_not_allowed", `Feature '${feature.name}' cannot use '${capabilityName}'`); + return async () => + fail( + "capability_not_allowed", + `Feature '${feature.name}' cannot use '${capabilityName}'`, + ); } - return target[capabilityName as keyof RuntimeCapabilities] ?? (async () => fail("capability_missing", `Runtime does not provide '${capabilityName}'`)); + return ( + target[capabilityName as keyof RuntimeCapabilities] ?? + (async () => fail("capability_missing", `Runtime does not provide '${capabilityName}'`)) + ); }, }); } - async function run(feature: Feature, input: Input, actor: Actor = { id: "demo-user" }, metadata: Record = {}): Promise> { - return feature.execute({ input, actor, metadata, capabilities: bindCapabilities(feature), ok, fail }); + async function run( + feature: Feature, + input: Input, + actor: Actor = { id: "demo-user" }, + metadata: Record = {}, + ): Promise> { + return feature.execute({ + input, + actor, + metadata, + capabilities: bindCapabilities(feature), + ok, + fail, + }); } return { auditLog, run }; } function createIdFactory(): IdFactory { - return { next: () => globalThis.crypto?.randomUUID?.() ?? `task-${Date.now()}-${Math.random().toString(16).slice(2)}` }; + return { + next: () => + globalThis.crypto?.randomUUID?.() ?? + `task-${Date.now()}-${Math.random().toString(16).slice(2)}`, + }; } diff --git a/examples/nodejs-react-typescript/backend/runtime/feature.ts b/examples/nodejs-react-typescript/backend/runtime/feature.ts index cbb9989..76f6a5e 100644 --- a/examples/nodejs-react-typescript/backend/runtime/feature.ts +++ b/examples/nodejs-react-typescript/backend/runtime/feature.ts @@ -30,13 +30,17 @@ export interface Feature { execute(context: FeatureContext): Promise>; } -export function defineFeature(definition: Feature): Readonly> { +export function defineFeature( + definition: Feature, +): Readonly> { const requiredFields = ["name", "description", "input", "output", "capabilities", "execute"]; for (const field of requiredFields) { if (!(field in definition)) throw new Error(`Feature is missing required field: ${field}`); } - if (!Array.isArray(definition.capabilities)) throw new Error("Feature capabilities must be an array"); - if (typeof definition.execute !== "function") throw new Error("Feature execute must be a function"); + if (!Array.isArray(definition.capabilities)) + throw new Error("Feature capabilities must be an array"); + if (typeof definition.execute !== "function") + throw new Error("Feature execute must be a function"); return Object.freeze({ ...definition }); } @@ -44,6 +48,10 @@ export function ok(value: Value): FeatureSuccess { return { ok: true, value }; } -export function fail(code: string, message: string, details: Record = {}): FeatureFailure { +export function fail( + code: string, + message: string, + details: Record = {}, +): FeatureFailure { return { ok: false, error: { code, message, details } }; } diff --git a/examples/nodejs-react-typescript/backend/server.ts b/examples/nodejs-react-typescript/backend/server.ts index 0817820..276a828 100644 --- a/examples/nodejs-react-typescript/backend/server.ts +++ b/examples/nodejs-react-typescript/backend/server.ts @@ -15,56 +15,117 @@ import type { FeatureResult } from "./runtime/feature"; const rootDir = join(fileURLToPath(new URL(".", import.meta.url)), ".."); const dataPath = join(rootDir, "data", "tasks.json"); const distDir = join(rootDir, "dist"); -const contentTypes: Record = { ".html": "text/html; charset=utf-8", ".css": "text/css; charset=utf-8", ".js": "text/javascript; charset=utf-8", ".json": "application/json; charset=utf-8", ".svg": "image/svg+xml" }; +const contentTypes: Record = { + ".html": "text/html; charset=utf-8", + ".css": "text/css; charset=utf-8", + ".js": "text/javascript; charset=utf-8", + ".json": "application/json; charset=utf-8", + ".svg": "image/svg+xml", +}; const runtime = createAifaRuntime({ taskStore: createFileTaskStore(dataPath) }); const server = createServer(async (request, response) => { try { writeCorsHeaders(response); - if (request.method === "OPTIONS") { response.writeHead(204); response.end(); return; } + if (request.method === "OPTIONS") { + response.writeHead(204); + response.end(); + return; + } const url = new URL(request.url ?? "/", `http://${request.headers.host ?? "127.0.0.1"}`); - if (url.pathname.startsWith("/api/")) { await handleApi(request, response, url); return; } + if (url.pathname.startsWith("/api/")) { + await handleApi(request, response, url); + return; + } await serveStatic(response, url.pathname); } catch (error: unknown) { - sendJson(response, 500, { ok: false, error: { code: "server_error", message: error instanceof Error ? error.message : "Unexpected server error" } }); + sendJson(response, 500, { + ok: false, + error: { + code: "server_error", + message: error instanceof Error ? error.message : "Unexpected server error", + }, + }); } }); -async function handleApi(request: IncomingMessage, response: ServerResponse, url: URL): Promise { +async function handleApi( + request: IncomingMessage, + response: ServerResponse, + url: URL, +): Promise { const demoUser = request.headers["x-demo-user"]; const actor = { id: typeof demoUser === "string" ? demoUser : "demo-user" }; - if (request.method === "GET" && url.pathname === "/api/tasks") { sendFeatureResult(response, await runtime.run(listTasks, {}, actor)); return; } - if (request.method === "POST" && url.pathname === "/api/tasks") { sendFeatureResult(response, await runtime.run(createTask, await readJson(request), actor), 201); return; } + if (request.method === "GET" && url.pathname === "/api/tasks") { + sendFeatureResult(response, await runtime.run(listTasks, {}, actor)); + return; + } + if (request.method === "POST" && url.pathname === "/api/tasks") { + sendFeatureResult(response, await runtime.run(createTask, await readJson(request), actor), 201); + return; + } const actionMatch = url.pathname.match(/^\/api\/tasks\/([^/]+)\/(complete|reopen)$/); if (request.method === "POST" && actionMatch) { const [, taskId, action] = actionMatch; - sendFeatureResult(response, action === "complete" ? await runtime.run(completeTask, { taskId }, actor) : await runtime.run(reopenTask, { taskId }, actor)); + sendFeatureResult( + response, + action === "complete" + ? await runtime.run(completeTask, { taskId }, actor) + : await runtime.run(reopenTask, { taskId }, actor), + ); return; } const deleteMatch = url.pathname.match(/^\/api\/tasks\/([^/]+)$/); - if (request.method === "DELETE" && deleteMatch) { sendFeatureResult(response, await runtime.run(deleteTask, { taskId: deleteMatch[1] }, actor)); return; } - sendJson(response, 404, { ok: false, error: { code: "route_not_found", message: "API route was not found" } }); + if (request.method === "DELETE" && deleteMatch) { + sendFeatureResult(response, await runtime.run(deleteTask, { taskId: deleteMatch[1] }, actor)); + return; + } + sendJson(response, 404, { + ok: false, + error: { code: "route_not_found", message: "API route was not found" }, + }); } async function serveStatic(response: ServerResponse, pathname: string): Promise { const requestedFile = pathname === "/" ? "index.html" : pathname.slice(1); const filePath = join(distDir, requestedFile); - response.writeHead(200, { "content-type": contentTypes[extname(filePath)] ?? "application/octet-stream" }); + response.writeHead(200, { + "content-type": contentTypes[extname(filePath)] ?? "application/octet-stream", + }); response.end(await readFile(filePath)); } async function readJson(request: IncomingMessage): Promise<{ title?: string; priority?: string }> { const chunks: Buffer[] = []; for await (const chunk of request) chunks.push(Buffer.from(chunk)); - return chunks.length === 0 ? {} : JSON.parse(Buffer.concat(chunks).toString("utf8")) as { title?: string; priority?: string }; + return chunks.length === 0 + ? {} + : (JSON.parse(Buffer.concat(chunks).toString("utf8")) as { title?: string; priority?: string }); } -function sendFeatureResult(response: ServerResponse, result: FeatureResult, successStatus = 200): void { - sendJson(response, result.ok ? successStatus : result.error.code === "not_found" ? 404 : 400, result); +function sendFeatureResult( + response: ServerResponse, + result: FeatureResult, + successStatus = 200, +): void { + sendJson( + response, + result.ok ? successStatus : result.error.code === "not_found" ? 404 : 400, + result, + ); +} +function sendJson(response: ServerResponse, status: number, payload: unknown): void { + response.writeHead(status, { "content-type": "application/json; charset=utf-8" }); + response.end(JSON.stringify(payload, null, 2)); +} +function writeCorsHeaders(response: ServerResponse): void { + response.setHeader("access-control-allow-origin", "*"); + response.setHeader("access-control-allow-methods", "GET,POST,DELETE,OPTIONS"); + response.setHeader("access-control-allow-headers", "content-type,x-demo-user"); } -function sendJson(response: ServerResponse, status: number, payload: unknown): void { response.writeHead(status, { "content-type": "application/json; charset=utf-8" }); response.end(JSON.stringify(payload, null, 2)); } -function writeCorsHeaders(response: ServerResponse): void { response.setHeader("access-control-allow-origin", "*"); response.setHeader("access-control-allow-methods", "GET,POST,DELETE,OPTIONS"); response.setHeader("access-control-allow-headers", "content-type,x-demo-user"); } const port = Number(process.env.PORT ?? 3000); const host = process.env.HOST ?? "127.0.0.1"; -server.listen(port, host, () => console.log(`AIFA React Slot App running at http://${host}:${port}`)); +server.listen(port, host, () => + console.log(`AIFA React Slot App running at http://${host}:${port}`), +); diff --git a/examples/nodejs-react-typescript/backend/storage/fileTaskStore.ts b/examples/nodejs-react-typescript/backend/storage/fileTaskStore.ts index d571e26..8658265 100644 --- a/examples/nodejs-react-typescript/backend/storage/fileTaskStore.ts +++ b/examples/nodejs-react-typescript/backend/storage/fileTaskStore.ts @@ -4,8 +4,9 @@ import type { Task, TaskStore } from "../types"; export function createFileTaskStore(filePath: string): TaskStore { async function readTasks(): Promise { - try { return JSON.parse(await readFile(filePath, "utf8")) as Task[]; } - catch (error: unknown) { + try { + return JSON.parse(await readFile(filePath, "utf8")) as Task[]; + } catch (error: unknown) { if (error instanceof Error && "code" in error && error.code === "ENOENT") return []; throw error; } @@ -15,10 +16,29 @@ export function createFileTaskStore(filePath: string): TaskStore { await writeFile(filePath, JSON.stringify(tasks, null, 2)); } return { - async create(task) { const tasks = await readTasks(); await writeTasks([...tasks, task]); return task; }, - async load(taskId) { return (await readTasks()).find((task) => task.id === taskId) ?? null; }, - async save(task) { const tasks = await readTasks(); await writeTasks(tasks.map((stored) => stored.id === task.id ? task : stored)); return task; }, - async delete(taskId) { const tasks = await readTasks(); const next = tasks.filter((task) => task.id !== taskId); await writeTasks(next); return next.length !== tasks.length; }, - async list() { return (await readTasks()).sort((left, right) => left.createdAt.localeCompare(right.createdAt)); }, + async create(task) { + const tasks = await readTasks(); + await writeTasks([...tasks, task]); + return task; + }, + async load(taskId) { + return (await readTasks()).find((task) => task.id === taskId) ?? null; + }, + async save(task) { + const tasks = await readTasks(); + await writeTasks(tasks.map((stored) => (stored.id === task.id ? task : stored))); + return task; + }, + async delete(taskId) { + const tasks = await readTasks(); + const next = tasks.filter((task) => task.id !== taskId); + await writeTasks(next); + return next.length !== tasks.length; + }, + async list() { + return (await readTasks()).sort((left, right) => + left.createdAt.localeCompare(right.createdAt), + ); + }, }; } diff --git a/examples/nodejs-react-typescript/backend/storage/memoryTaskStore.ts b/examples/nodejs-react-typescript/backend/storage/memoryTaskStore.ts index 910fbdc..3e4bac5 100644 --- a/examples/nodejs-react-typescript/backend/storage/memoryTaskStore.ts +++ b/examples/nodejs-react-typescript/backend/storage/memoryTaskStore.ts @@ -3,10 +3,24 @@ import type { Task, TaskStore } from "../types"; export function createMemoryTaskStore(seed: Task[] = []): TaskStore { const tasks = new Map(seed.map((task) => [task.id, task])); return { - async create(task) { tasks.set(task.id, task); return task; }, - async load(taskId) { return tasks.get(taskId) ?? null; }, - async save(task) { tasks.set(task.id, task); return task; }, - async delete(taskId) { return tasks.delete(taskId); }, - async list() { return [...tasks.values()].sort((left, right) => left.createdAt.localeCompare(right.createdAt)); }, + async create(task) { + tasks.set(task.id, task); + return task; + }, + async load(taskId) { + return tasks.get(taskId) ?? null; + }, + async save(task) { + tasks.set(task.id, task); + return task; + }, + async delete(taskId) { + return tasks.delete(taskId); + }, + async list() { + return [...tasks.values()].sort((left, right) => + left.createdAt.localeCompare(right.createdAt), + ); + }, }; } diff --git a/examples/nodejs-react-typescript/examples/tickets/create-task.md b/examples/nodejs-react-typescript/examples/tickets/create-task.md index 18eeab6..06e66d7 100644 --- a/examples/nodejs-react-typescript/examples/tickets/create-task.md +++ b/examples/nodejs-react-typescript/examples/tickets/create-task.md @@ -37,4 +37,3 @@ Allow an actor to create one active task from the React UI. - Trims the title. - Defaults unknown priority to `medium`. - Rejects empty titles. - diff --git a/examples/nodejs-react-typescript/examples/tickets/ui-slots.md b/examples/nodejs-react-typescript/examples/tickets/ui-slots.md index 1cf3612..99f08eb 100644 --- a/examples/nodejs-react-typescript/examples/tickets/ui-slots.md +++ b/examples/nodejs-react-typescript/examples/tickets/ui-slots.md @@ -31,4 +31,3 @@ Each contribution has: The application owns stable extension points, while add-ons contribute UI independently. In AIFA terms, it makes frontend customization atomic too. - diff --git a/examples/nodejs-react-typescript/frontend/src/api.ts b/examples/nodejs-react-typescript/frontend/src/api.ts index 41dc996..a319cef 100644 --- a/examples/nodejs-react-typescript/frontend/src/api.ts +++ b/examples/nodejs-react-typescript/frontend/src/api.ts @@ -6,14 +6,19 @@ export function listTasks(): Promise> { return request("/api/tasks"); } -export function createTask(task: Pick): Promise> { +export function createTask( + task: Pick, +): Promise> { return request("/api/tasks", { method: "POST", body: JSON.stringify(task), }); } -export function setTaskCompleted(taskId: string, completed: boolean): Promise> { +export function setTaskCompleted( + taskId: string, + completed: boolean, +): Promise> { return request(`/api/tasks/${taskId}/${completed ? "complete" : "reopen"}`, { method: "POST", }); diff --git a/examples/nodejs-react-typescript/frontend/src/contributions/registerContributions.tsx b/examples/nodejs-react-typescript/frontend/src/contributions/registerContributions.tsx index 6f147d1..211e4d4 100644 --- a/examples/nodejs-react-typescript/frontend/src/contributions/registerContributions.tsx +++ b/examples/nodejs-react-typescript/frontend/src/contributions/registerContributions.tsx @@ -5,7 +5,11 @@ registerSlotContribution({ name: "refresh-button", order: 10, render: ({ model }) => ( - ), @@ -48,7 +52,9 @@ registerSlotContribution({ name: "high-priority-count", order: 30, render: ({ model }) => { - const highPriorityCount = model.tasks.filter((task) => task.priority === "high" && !task.completed).length; + const highPriorityCount = model.tasks.filter( + (task) => task.priority === "high" && !task.completed, + ).length; return (
      @@ -69,7 +75,11 @@ registerSlotContribution({ Reopen ) : ( - ), @@ -91,7 +101,11 @@ registerSlotContribution({ name: "create-example-task", order: 10, render: ({ model }) => ( - ), diff --git a/examples/nodejs-react-typescript/frontend/src/main.tsx b/examples/nodejs-react-typescript/frontend/src/main.tsx index 2457ea2..303be87 100644 --- a/examples/nodejs-react-typescript/frontend/src/main.tsx +++ b/examples/nodejs-react-typescript/frontend/src/main.tsx @@ -25,7 +25,9 @@ function App() { } useEffect(() => { - void refreshTasks().catch((error: unknown) => setStatus(error instanceof Error ? error.message : "Request failed")); + void refreshTasks().catch((error: unknown) => + setStatus(error instanceof Error ? error.message : "Request failed"), + ); }, []); const visibleTasks = useMemo(() => { @@ -131,7 +133,12 @@ function App() {
        {visibleTasks.map((task) => ( - + ))}
      @@ -149,7 +156,10 @@ function App() {
      Active feature {activeFeature} -

      React triggers HTTP. The Node adapter runs one AIFA feature. Runtime provides capabilities.

      +

      + React triggers HTTP. The Node adapter runs one AIFA feature. Runtime provides + capabilities. +

      Frontend slot model diff --git a/examples/nodejs-react-typescript/frontend/src/slots/Slot.tsx b/examples/nodejs-react-typescript/frontend/src/slots/Slot.tsx index f02cb6b..e178529 100644 --- a/examples/nodejs-react-typescript/frontend/src/slots/Slot.tsx +++ b/examples/nodejs-react-typescript/frontend/src/slots/Slot.tsx @@ -11,7 +11,12 @@ export function Slot({ name, model }: SlotProperties ( - + {contribution.render({ model, contribution })} )); diff --git a/examples/nodejs-react-typescript/frontend/src/slots/slotRegistry.ts b/examples/nodejs-react-typescript/frontend/src/slots/slotRegistry.ts index 796d6dc..6d06bf3 100644 --- a/examples/nodejs-react-typescript/frontend/src/slots/slotRegistry.ts +++ b/examples/nodejs-react-typescript/frontend/src/slots/slotRegistry.ts @@ -6,21 +6,30 @@ export interface SlotContribution { slot: Name; name: string; order?: number; - render: (properties: { model: SlotModels[Name]; contribution: SlotContribution }) => ReactNode; + render: (properties: { + model: SlotModels[Name]; + contribution: SlotContribution; + }) => ReactNode; } const contributions = new Map(); -export function registerSlotContribution(contribution: SlotContribution): void { +export function registerSlotContribution( + contribution: SlotContribution, +): void { const normalizedContribution: SlotContribution = { order: 100, ...contribution, }; - const slotContributions = (contributions.get(contribution.slot) ?? []) as SlotContribution[]; + const slotContributions = (contributions.get(contribution.slot) ?? + []) as SlotContribution[]; const nextContributions = [ ...slotContributions.filter((candidate) => candidate.name !== contribution.name), normalizedContribution, - ].sort((left, right) => (left.order ?? 100) - (right.order ?? 100) || left.name.localeCompare(right.name)); + ].sort( + (left, right) => + (left.order ?? 100) - (right.order ?? 100) || left.name.localeCompare(right.name), + ); contributions.set(contribution.slot, nextContributions); } diff --git a/examples/nodejs-react-typescript/frontend/src/styles.css b/examples/nodejs-react-typescript/frontend/src/styles.css index 18b34ea..f7a00b7 100644 --- a/examples/nodejs-react-typescript/frontend/src/styles.css +++ b/examples/nodejs-react-typescript/frontend/src/styles.css @@ -23,7 +23,12 @@ body { linear-gradient(135deg, #eff7f2, var(--paper)); color: var(--ink); font-family: - Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", + Inter, + ui-sans-serif, + system-ui, + -apple-system, + BlinkMacSystemFont, + "Segoe UI", sans-serif; } @@ -420,4 +425,3 @@ h2 { position: static; } } - diff --git a/examples/nodejs-react-typescript/test/slotRegistry.test.js b/examples/nodejs-react-typescript/test/slotRegistry.test.js index f955cb3..41a6577 100644 --- a/examples/nodejs-react-typescript/test/slotRegistry.test.js +++ b/examples/nodejs-react-typescript/test/slotRegistry.test.js @@ -1,7 +1,10 @@ import assert from "node:assert/strict"; import test from "node:test"; -import { getSlotContributions, registerSlotContribution } from "../frontend/src/slots/slotRegistry.ts"; +import { + getSlotContributions, + registerSlotContribution, +} from "../frontend/src/slots/slotRegistry.ts"; test("sorts slot contributions by order", () => { registerSlotContribution({ diff --git a/examples/nodejs-react-typescript/test/taskFeatures.test.js b/examples/nodejs-react-typescript/test/taskFeatures.test.js index 8062c64..e3206c2 100644 --- a/examples/nodejs-react-typescript/test/taskFeatures.test.js +++ b/examples/nodejs-react-typescript/test/taskFeatures.test.js @@ -32,7 +32,11 @@ function createRuntime(seed = []) { test("creates an active task with priority", async () => { const runtime = createRuntime(); - const result = await runtime.run(createTask, { title: " Build slot UI ", priority: "high" }, actor); + const result = await runtime.run( + createTask, + { title: " Build slot UI ", priority: "high" }, + actor, + ); assert.equal(result.ok, true); assert.equal(result.value.task.id, "task-1"); @@ -45,7 +49,11 @@ test("creates an active task with priority", async () => { test("defaults unknown priority to medium", async () => { const runtime = createRuntime(); - const result = await runtime.run(createTask, { title: "Review contract", priority: "urgent" }, actor); + const result = await runtime.run( + createTask, + { title: "Review contract", priority: "urgent" }, + actor, + ); assert.equal(result.value.task.priority, "medium"); }); diff --git a/examples/tickets/archive-project.md b/examples/tickets/archive-project.md index bd8b41d..96495f3 100644 --- a/examples/tickets/archive-project.md +++ b/examples/tickets/archive-project.md @@ -38,4 +38,3 @@ Allow an authorized actor to archive one project. - Returns success for an already archived project. - Rejects an actor without archive permission. - Returns `not_found` for a missing project. - diff --git a/examples/vanilla-js/README.md b/examples/vanilla-js/README.md index 5e70584..61df811 100644 --- a/examples/vanilla-js/README.md +++ b/examples/vanilla-js/README.md @@ -29,13 +29,13 @@ http://localhost:3000 Each app action maps to one AIFA feature: -| UI Action | API Route | Atomic Feature | -| --- | --- | --- | -| Create task | `POST /api/tasks` | `createTask` | -| List tasks | `GET /api/tasks` | `listTasks` | +| UI Action | API Route | Atomic Feature | +| ------------- | ------------------------------ | -------------- | +| Create task | `POST /api/tasks` | `createTask` | +| List tasks | `GET /api/tasks` | `listTasks` | | Complete task | `POST /api/tasks/:id/complete` | `completeTask` | -| Reopen task | `POST /api/tasks/:id/reopen` | `reopenTask` | -| Delete task | `DELETE /api/tasks/:id` | `deleteTask` | +| Reopen task | `POST /api/tasks/:id/reopen` | `reopenTask` | +| Delete task | `DELETE /api/tasks/:id` | `deleteTask` | The feature does not know HTTP, files, local storage, routing, or the frontend. @@ -76,4 +76,3 @@ With AIFA, the implementation target is small: 4. run behavioral tests The runtime owns infrastructure. The feature owns behavior. - diff --git a/examples/vanilla-js/backend/features/completeTask.js b/examples/vanilla-js/backend/features/completeTask.js index 5af0b76..f6edf83 100644 --- a/examples/vanilla-js/backend/features/completeTask.js +++ b/examples/vanilla-js/backend/features/completeTask.js @@ -42,4 +42,3 @@ export const completeTask = defineFeature({ return context.ok({ task: completedTask }); }, }); - diff --git a/examples/vanilla-js/backend/features/createTask.js b/examples/vanilla-js/backend/features/createTask.js index 154b70a..e1f1c0c 100644 --- a/examples/vanilla-js/backend/features/createTask.js +++ b/examples/vanilla-js/backend/features/createTask.js @@ -37,4 +37,3 @@ export const createTask = defineFeature({ return context.ok({ task }); }, }); - diff --git a/examples/vanilla-js/backend/features/deleteTask.js b/examples/vanilla-js/backend/features/deleteTask.js index 07c4936..3c77f96 100644 --- a/examples/vanilla-js/backend/features/deleteTask.js +++ b/examples/vanilla-js/backend/features/deleteTask.js @@ -34,4 +34,3 @@ export const deleteTask = defineFeature({ return context.ok({ deleted: true }); }, }); - diff --git a/examples/vanilla-js/backend/features/listTasks.js b/examples/vanilla-js/backend/features/listTasks.js index 39b97d4..4acc571 100644 --- a/examples/vanilla-js/backend/features/listTasks.js +++ b/examples/vanilla-js/backend/features/listTasks.js @@ -15,4 +15,3 @@ export const listTasks = defineFeature({ return context.ok({ tasks }); }, }); - diff --git a/examples/vanilla-js/backend/features/reopenTask.js b/examples/vanilla-js/backend/features/reopenTask.js index b6a3875..0b89cf1 100644 --- a/examples/vanilla-js/backend/features/reopenTask.js +++ b/examples/vanilla-js/backend/features/reopenTask.js @@ -42,4 +42,3 @@ export const reopenTask = defineFeature({ return context.ok({ task: reopenedTask }); }, }); - diff --git a/examples/vanilla-js/backend/runtime/aifaRuntime.js b/examples/vanilla-js/backend/runtime/aifaRuntime.js index b6c8d8a..fc1ce0d 100644 --- a/examples/vanilla-js/backend/runtime/aifaRuntime.js +++ b/examples/vanilla-js/backend/runtime/aifaRuntime.js @@ -1,6 +1,10 @@ import { fail, ok } from "./feature.js"; -export function createAifaRuntime({ taskStore, idFactory = createIdFactory(), clock = () => new Date() }) { +export function createAifaRuntime({ + taskStore, + idFactory = createIdFactory(), + clock = () => new Date(), +}) { const auditLog = []; const allCapabilities = { @@ -32,13 +36,17 @@ export function createAifaRuntime({ taskStore, idFactory = createIdFactory(), cl if (!allowed.has(capabilityName)) { return async () => - fail("capability_not_allowed", `Feature '${feature.name}' cannot use '${capabilityName}'`); + fail( + "capability_not_allowed", + `Feature '${feature.name}' cannot use '${capabilityName}'`, + ); } const capability = allCapabilities[capabilityName]; if (!capability) { - return async () => fail("capability_missing", `Runtime does not provide '${capabilityName}'`); + return async () => + fail("capability_missing", `Runtime does not provide '${capabilityName}'`); } return capability; @@ -75,4 +83,3 @@ function createIdFactory() { }, }; } - diff --git a/examples/vanilla-js/backend/runtime/feature.js b/examples/vanilla-js/backend/runtime/feature.js index b6ce344..559307e 100644 --- a/examples/vanilla-js/backend/runtime/feature.js +++ b/examples/vanilla-js/backend/runtime/feature.js @@ -35,4 +35,3 @@ export function fail(code, message, details = {}) { }, }; } - diff --git a/examples/vanilla-js/backend/storage/fileTaskStore.js b/examples/vanilla-js/backend/storage/fileTaskStore.js index 3334d8b..c0a142c 100644 --- a/examples/vanilla-js/backend/storage/fileTaskStore.js +++ b/examples/vanilla-js/backend/storage/fileTaskStore.js @@ -47,4 +47,3 @@ export function createFileTaskStore(filePath) { }, }; } - diff --git a/examples/vanilla-js/backend/storage/memoryTaskStore.js b/examples/vanilla-js/backend/storage/memoryTaskStore.js index 16793ce..124b75c 100644 --- a/examples/vanilla-js/backend/storage/memoryTaskStore.js +++ b/examples/vanilla-js/backend/storage/memoryTaskStore.js @@ -17,8 +17,9 @@ export function createMemoryTaskStore(seed = []) { return tasks.delete(taskId); }, async list() { - return [...tasks.values()].sort((left, right) => left.createdAt.localeCompare(right.createdAt)); + return [...tasks.values()].sort((left, right) => + left.createdAt.localeCompare(right.createdAt), + ); }, }; } - diff --git a/examples/vanilla-js/examples/tickets/complete-task.md b/examples/vanilla-js/examples/tickets/complete-task.md index d140612..14e73b6 100644 --- a/examples/vanilla-js/examples/tickets/complete-task.md +++ b/examples/vanilla-js/examples/tickets/complete-task.md @@ -35,4 +35,3 @@ Allow an actor to complete one existing task. - Completes an active task. - Returns success for an already completed task. - Returns `not_found` for a missing task. - diff --git a/examples/vanilla-js/examples/tickets/create-task.md b/examples/vanilla-js/examples/tickets/create-task.md index 9c461da..92c96e7 100644 --- a/examples/vanilla-js/examples/tickets/create-task.md +++ b/examples/vanilla-js/examples/tickets/create-task.md @@ -34,4 +34,3 @@ Allow an actor to create one active task. - Creates an active task. - Trims the provided title. - Rejects an empty title. - diff --git a/examples/vanilla-js/examples/tickets/delete-task.md b/examples/vanilla-js/examples/tickets/delete-task.md index 91173f6..5951c4d 100644 --- a/examples/vanilla-js/examples/tickets/delete-task.md +++ b/examples/vanilla-js/examples/tickets/delete-task.md @@ -31,4 +31,3 @@ Allow an actor to delete one existing task. - Deletes an existing task. - Returns `not_found` for a missing task. - diff --git a/examples/vanilla-js/examples/tickets/reopen-task.md b/examples/vanilla-js/examples/tickets/reopen-task.md index 1e3a7d8..8e94257 100644 --- a/examples/vanilla-js/examples/tickets/reopen-task.md +++ b/examples/vanilla-js/examples/tickets/reopen-task.md @@ -34,4 +34,3 @@ Allow an actor to reopen one completed task. - Reopens a completed task. - Returns success for an active task. - Returns `not_found` for a missing task. - diff --git a/examples/vanilla-js/frontend/app.js b/examples/vanilla-js/frontend/app.js index d0a2aae..99f1411 100644 --- a/examples/vanilla-js/frontend/app.js +++ b/examples/vanilla-js/frontend/app.js @@ -11,8 +11,10 @@ let activeFilter = "all"; const traceCopy = { "list-tasks": "UI asks for task state. API runs listTasks. Runtime provides task.list.", - "create-task": "UI sends a title. API runs createTask. Runtime provides id, clock, storage, and audit.", - "complete-task": "UI toggles a task. API runs completeTask. Runtime loads, saves, timestamps, and audits.", + "create-task": + "UI sends a title. API runs createTask. Runtime provides id, clock, storage, and audit.", + "complete-task": + "UI toggles a task. API runs completeTask. Runtime loads, saves, timestamps, and audits.", "reopen-task": "UI toggles a done task. API runs reopenTask. Runtime loads, saves, and audits.", "delete-task": "UI deletes a task. API runs deleteTask. Runtime loads, deletes, and audits.", }; @@ -39,7 +41,9 @@ taskForm.addEventListener("submit", async (event) => { filterButtons.forEach((button) => { button.addEventListener("click", () => { activeFilter = button.dataset.filter; - filterButtons.forEach((candidate) => candidate.classList.toggle("active", candidate === button)); + filterButtons.forEach((candidate) => + candidate.classList.toggle("active", candidate === button), + ); render(); }); }); @@ -127,4 +131,3 @@ function setStatus(message) { loadTasks().catch((error) => { setStatus(error.message); }); - diff --git a/examples/vanilla-js/frontend/index.html b/examples/vanilla-js/frontend/index.html index 5492572..c735dc0 100644 --- a/examples/vanilla-js/frontend/index.html +++ b/examples/vanilla-js/frontend/index.html @@ -18,7 +18,13 @@

      Task Board

      - +
      @@ -60,4 +66,3 @@

      Every action calls one feature.

      - diff --git a/examples/vanilla-js/frontend/styles.css b/examples/vanilla-js/frontend/styles.css index 5395d82..3b8572e 100644 --- a/examples/vanilla-js/frontend/styles.css +++ b/examples/vanilla-js/frontend/styles.css @@ -18,11 +18,15 @@ body { min-height: 100vh; background: radial-gradient(circle at 20% 10%, rgba(46, 196, 154, 0.18), transparent 28%), - radial-gradient(circle at 90% 80%, rgba(242, 107, 91, 0.16), transparent 26%), - var(--paper); + radial-gradient(circle at 90% 80%, rgba(242, 107, 91, 0.16), transparent 26%), var(--paper); color: var(--ink); font-family: - Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", + Inter, + ui-sans-serif, + system-ui, + -apple-system, + BlinkMacSystemFont, + "Segoe UI", sans-serif; } @@ -277,4 +281,3 @@ h2 { position: static; } } - diff --git a/examples/vanilla-js/package.json b/examples/vanilla-js/package.json index 994805f..dbc231a 100644 --- a/examples/vanilla-js/package.json +++ b/examples/vanilla-js/package.json @@ -12,4 +12,3 @@ "node": ">=20" } } - diff --git a/examples/vanilla-js/test/taskFeatures.test.js b/examples/vanilla-js/test/taskFeatures.test.js index 5bccdfb..384201a 100644 --- a/examples/vanilla-js/test/taskFeatures.test.js +++ b/examples/vanilla-js/test/taskFeatures.test.js @@ -127,4 +127,3 @@ function createSeedTask(id, title, createdAt = "2026-08-14T10:00:00.000Z") { completedAt: null, }; } - diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..fb7f479 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,34 @@ +{ + "name": "ai-atomic-feature-architecture", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "ai-atomic-feature-architecture", + "version": "0.1.0", + "devDependencies": { + "prettier": "^3.9.6" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/prettier": { + "version": "3.9.6", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.9.6.tgz", + "integrity": "sha512-OpN0zzVdiaiAhxpuuj5efpIS4sY9j7bY6uR5mnj5yPzGkdkjNKSJeUThPb60Jw29QuAZgA4o+/iB49kFiaBX6g==", + "dev": true, + "license": "MIT", + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + } + } +} diff --git a/package.json b/package.json index ffb4b48..349d27a 100644 --- a/package.json +++ b/package.json @@ -10,6 +10,8 @@ }, "engines": { "node": ">=20" + }, + "devDependencies": { + "prettier": "^3.9.6" } } - diff --git a/src/demo.js b/src/demo.js index bf611e4..cdfdb0f 100644 --- a/src/demo.js +++ b/src/demo.js @@ -21,4 +21,3 @@ const actor = { const result = await runtime.run(archiveProject, { projectId: "project-1" }, actor); console.log(JSON.stringify(result, null, 2)); - diff --git a/src/features/archiveProject.js b/src/features/archiveProject.js index 850bcec..b5b799b 100644 --- a/src/features/archiveProject.js +++ b/src/features/archiveProject.js @@ -56,4 +56,3 @@ export const archiveProject = defineFeature({ }); }, }); - diff --git a/src/runtime/feature.js b/src/runtime/feature.js index b6ce344..559307e 100644 --- a/src/runtime/feature.js +++ b/src/runtime/feature.js @@ -35,4 +35,3 @@ export function fail(code, message, details = {}) { }, }; } - diff --git a/src/runtime/memoryRuntime.js b/src/runtime/memoryRuntime.js index 1c641d2..8adeb69 100644 --- a/src/runtime/memoryRuntime.js +++ b/src/runtime/memoryRuntime.js @@ -13,7 +13,9 @@ export function createMemoryRuntime(seed = {}) { return project; }, "permission.check": async ({ actor, permission, resource }) => { - return actor.permissions?.includes(permission) && resource.organizationId === actor.organizationId; + return ( + actor.permissions?.includes(permission) && resource.organizationId === actor.organizationId + ); }, "audit.record": async (entry) => { state.auditLog.push({ @@ -36,12 +38,16 @@ export function createMemoryRuntime(seed = {}) { if (!allowed.has(capabilityName)) { return async () => - fail("capability_not_allowed", `Feature '${feature.name}' cannot use '${capabilityName}'`); + fail( + "capability_not_allowed", + `Feature '${feature.name}' cannot use '${capabilityName}'`, + ); } const capability = allCapabilities[capabilityName]; if (!capability) { - return async () => fail("capability_missing", `Runtime does not provide '${capabilityName}'`); + return async () => + fail("capability_missing", `Runtime does not provide '${capabilityName}'`); } return capability; @@ -68,4 +74,3 @@ export function createMemoryRuntime(seed = {}) { state, }; } - diff --git a/test/archiveProject.test.js b/test/archiveProject.test.js index cbf5caf..80e8aac 100644 --- a/test/archiveProject.test.js +++ b/test/archiveProject.test.js @@ -78,4 +78,3 @@ test("returns not_found for missing projects", async () => { assert.equal(result.ok, false); assert.equal(result.error.code, "not_found"); }); - From 8a6c1ae900f908c4850ca1a8536d7fa361c328e0 Mon Sep 17 00:00:00 2001 From: Adrian Besleaga Date: Sat, 15 Aug 2026 14:28:28 +0200 Subject: [PATCH 4/6] Update readme and demo project --- .prettierrc.json | 3 - README.md | 406 ++- demo/ai-todo-assistant/README.md | 4 +- docs/architecture.md | 191 +- docs/index.html | 533 ++-- docs/research-plan.md | 18 +- docs/styles.css | 1312 +++++++--- docs/ticket-contract.md | 58 +- examples/nodejs-react-typescript/README.md | 115 - .../backend/features/completeTask.ts | 30 - .../backend/features/createTask.ts | 36 - .../backend/features/deleteTask.ts | 23 - .../backend/features/listTasks.ts | 12 - .../backend/features/reopenTask.ts | 26 - .../backend/runtime/aifaRuntime.ts | 77 - .../backend/runtime/feature.ts | 57 - .../nodejs-react-typescript/backend/server.ts | 131 - .../backend/storage/fileTaskStore.ts | 44 - .../backend/storage/memoryTaskStore.ts | 26 - .../nodejs-react-typescript/backend/types.ts | 40 - .../examples/tickets/create-task.md | 39 - .../examples/tickets/ui-slots.md | 33 - .../frontend/index.html | 12 - .../frontend/src/api.ts | 48 - .../frontend/src/components/TaskRow.tsx | 37 - .../contributions/registerContributions.tsx | 112 - .../frontend/src/main.tsx | 186 -- .../frontend/src/slots/Slot.tsx | 23 - .../frontend/src/slots/slotRegistry.ts | 39 - .../frontend/src/styles.css | 427 ---- .../frontend/src/types.ts | 56 - .../frontend/src/vite-env.d.ts | 1 - .../nodejs-react-typescript/package-lock.json | 2254 ----------------- examples/nodejs-react-typescript/package.json | 30 - .../test/slotRegistry.test.js | 48 - .../test/taskFeatures.test.js | 123 - .../nodejs-react-typescript/tsconfig.json | 20 - examples/tickets/archive-project.md | 40 - examples/vanilla-js/README.md | 78 - .../backend/features/completeTask.js | 44 - .../vanilla-js/backend/features/createTask.js | 39 - .../vanilla-js/backend/features/deleteTask.js | 36 - .../vanilla-js/backend/features/listTasks.js | 17 - .../vanilla-js/backend/features/reopenTask.js | 44 - .../vanilla-js/backend/runtime/aifaRuntime.js | 85 - .../vanilla-js/backend/runtime/feature.js | 37 - examples/vanilla-js/backend/server.js | 136 - .../backend/storage/fileTaskStore.js | 49 - .../backend/storage/memoryTaskStore.js | 25 - .../examples/tickets/complete-task.md | 37 - .../examples/tickets/create-task.md | 36 - .../examples/tickets/delete-task.md | 33 - .../examples/tickets/reopen-task.md | 36 - examples/vanilla-js/frontend/app.js | 133 - examples/vanilla-js/frontend/index.html | 68 - examples/vanilla-js/frontend/styles.css | 283 --- examples/vanilla-js/package.json | 14 - examples/vanilla-js/test/taskFeatures.test.js | 129 - package-lock.json | 34 - package.json | 17 - src/demo.js | 23 - src/features/archiveProject.js | 58 - src/runtime/feature.js | 37 - src/runtime/memoryRuntime.js | 76 - test/archiveProject.test.js | 80 - 65 files changed, 1621 insertions(+), 6733 deletions(-) delete mode 100644 .prettierrc.json delete mode 100644 examples/nodejs-react-typescript/README.md delete mode 100644 examples/nodejs-react-typescript/backend/features/completeTask.ts delete mode 100644 examples/nodejs-react-typescript/backend/features/createTask.ts delete mode 100644 examples/nodejs-react-typescript/backend/features/deleteTask.ts delete mode 100644 examples/nodejs-react-typescript/backend/features/listTasks.ts delete mode 100644 examples/nodejs-react-typescript/backend/features/reopenTask.ts delete mode 100644 examples/nodejs-react-typescript/backend/runtime/aifaRuntime.ts delete mode 100644 examples/nodejs-react-typescript/backend/runtime/feature.ts delete mode 100644 examples/nodejs-react-typescript/backend/server.ts delete mode 100644 examples/nodejs-react-typescript/backend/storage/fileTaskStore.ts delete mode 100644 examples/nodejs-react-typescript/backend/storage/memoryTaskStore.ts delete mode 100644 examples/nodejs-react-typescript/backend/types.ts delete mode 100644 examples/nodejs-react-typescript/examples/tickets/create-task.md delete mode 100644 examples/nodejs-react-typescript/examples/tickets/ui-slots.md delete mode 100644 examples/nodejs-react-typescript/frontend/index.html delete mode 100644 examples/nodejs-react-typescript/frontend/src/api.ts delete mode 100644 examples/nodejs-react-typescript/frontend/src/components/TaskRow.tsx delete mode 100644 examples/nodejs-react-typescript/frontend/src/contributions/registerContributions.tsx delete mode 100644 examples/nodejs-react-typescript/frontend/src/main.tsx delete mode 100644 examples/nodejs-react-typescript/frontend/src/slots/Slot.tsx delete mode 100644 examples/nodejs-react-typescript/frontend/src/slots/slotRegistry.ts delete mode 100644 examples/nodejs-react-typescript/frontend/src/styles.css delete mode 100644 examples/nodejs-react-typescript/frontend/src/types.ts delete mode 100644 examples/nodejs-react-typescript/frontend/src/vite-env.d.ts delete mode 100644 examples/nodejs-react-typescript/package-lock.json delete mode 100644 examples/nodejs-react-typescript/package.json delete mode 100644 examples/nodejs-react-typescript/test/slotRegistry.test.js delete mode 100644 examples/nodejs-react-typescript/test/taskFeatures.test.js delete mode 100644 examples/nodejs-react-typescript/tsconfig.json delete mode 100644 examples/tickets/archive-project.md delete mode 100644 examples/vanilla-js/README.md delete mode 100644 examples/vanilla-js/backend/features/completeTask.js delete mode 100644 examples/vanilla-js/backend/features/createTask.js delete mode 100644 examples/vanilla-js/backend/features/deleteTask.js delete mode 100644 examples/vanilla-js/backend/features/listTasks.js delete mode 100644 examples/vanilla-js/backend/features/reopenTask.js delete mode 100644 examples/vanilla-js/backend/runtime/aifaRuntime.js delete mode 100644 examples/vanilla-js/backend/runtime/feature.js delete mode 100644 examples/vanilla-js/backend/server.js delete mode 100644 examples/vanilla-js/backend/storage/fileTaskStore.js delete mode 100644 examples/vanilla-js/backend/storage/memoryTaskStore.js delete mode 100644 examples/vanilla-js/examples/tickets/complete-task.md delete mode 100644 examples/vanilla-js/examples/tickets/create-task.md delete mode 100644 examples/vanilla-js/examples/tickets/delete-task.md delete mode 100644 examples/vanilla-js/examples/tickets/reopen-task.md delete mode 100644 examples/vanilla-js/frontend/app.js delete mode 100644 examples/vanilla-js/frontend/index.html delete mode 100644 examples/vanilla-js/frontend/styles.css delete mode 100644 examples/vanilla-js/package.json delete mode 100644 examples/vanilla-js/test/taskFeatures.test.js delete mode 100644 package-lock.json delete mode 100644 package.json delete mode 100644 src/demo.js delete mode 100644 src/features/archiveProject.js delete mode 100644 src/runtime/feature.js delete mode 100644 src/runtime/memoryRuntime.js delete mode 100644 test/archiveProject.test.js diff --git a/.prettierrc.json b/.prettierrc.json deleted file mode 100644 index de753c5..0000000 --- a/.prettierrc.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "printWidth": 100 -} diff --git a/README.md b/README.md index 09cb486..e89fc86 100644 --- a/README.md +++ b/README.md @@ -1,275 +1,271 @@ -# AI Atomic Feature Architecture +# AIFA — AI Atomic Feature Architecture -AI Atomic Feature Architecture, or AIFA, is an experiment in making software systems from small, explicit, independently implementable features. +**AIFA is a way to organize software so people and AI agents can build features together with less confusion, less risk, and less time spent understanding unrelated code.** -The core question: +It gives every feature a clear brief: what it should do, what information it receives, what systems it may use, what can go wrong, and how success is tested. -> Can a developer or AI agent implement one feature without needing to understand the whole project? +> **See it in a real application:** [Focusly, the AI Todo Assistant](./demo/ai-todo-assistant/README.md), is the canonical AIFA example. -AIFA tries to make the answer "yes" by turning each feature into a self-contained contract. +![Focusly task workspace](./demo/ai-todo-assistant/public/readme-workspace.jpg) -## The Idea +# Part I — The business story -Most codebases ask contributors to learn the surrounding system before they can safely change it. AIFA moves that knowledge into a runtime contract: +## The problem AIFA solves -- A feature receives one `context` object. -- The context contains input, actor, metadata, and allowed capabilities. -- The feature cannot import infrastructure directly. -- The runtime owns databases, queues, auth, logs, and transactions. -- A ticket describes the feature contract before implementation begins. +In a typical software project, a request that sounds simple—“let users create a task”—can require someone to understand the database, security, APIs, screens, coding conventions, and other features before making a safe change. -```txt -Ticket Contract - | - v -Feature(input, capabilities) - | - v -Runtime-owned infrastructure -``` +That creates familiar business problems: -## Repository Contents +- new work takes longer than expected; +- estimates are unreliable because hidden dependencies appear late; +- several people editing shared code create conflicts; +- reviews are difficult because a change spreads across the system; +- AI can produce code quickly, but may miss rules it was never told about; +- knowledge stays in the heads of experienced team members. -- `src/runtime/feature.js` defines the feature contract. -- `src/runtime/memoryRuntime.js` provides a tiny in-memory runtime. -- `src/features/archiveProject.js` is an atomic feature. -- `src/demo.js` runs the PoC end to end. -- `test/archiveProject.test.js` verifies the feature through the runtime. -- `docs/architecture.md` explains the architectural model. -- `docs/ticket-contract.md` defines the ticket format. -- `docs/research-plan.md` proposes how to evaluate the idea. -- `docs/index.html` is a GitHub Pages presentation site. -- `examples/tickets/archive-project.md` shows one complete feature ticket. +AIFA moves that hidden knowledge into an explicit feature contract. -## GitHub Pages +## Why it is good for a business -This repository includes a static presentation page in `docs/`. +| Business need | How AIFA helps | +| --- | --- | +| Deliver faster | A feature can be understood and implemented without exploring the whole application first. | +| Reduce risk | Permissions, allowed system access, errors, and acceptance tests are stated before implementation. | +| Work in parallel | Teams and AI agents can own separate feature folders with less overlap and fewer merge conflicts. | +| Improve predictability | The scope and completion conditions are visible at the beginning of the work. | +| Preserve knowledge | Important rules live in versioned contracts instead of only in meetings or people’s memory. | +| Change technology safely | Product behavior depends on stable capabilities, not directly on a database or vendor SDK. | -Enable it in GitHub: +AIFA does not promise that software becomes effortless. It makes complexity visible and puts it in deliberate places, where it can be reviewed and tested. -1. Open repository settings. -2. Go to Pages. -3. Set source to `Deploy from a branch`. -4. Select `main` and `/docs`. +## One feature, one shared understanding -The site will be available at: +Imagine the business asks for this: -```txt -https://adrianbesleaga.github.io/AIFA/ -``` +> Let a person or an approved AI suggestion create a categorized task. -## Run The PoC +Before coding starts, an AIFA feature answers: -Requires Node.js 20 or newer. +- What information is required? +- Who is allowed to do it? +- Which business rules apply? +- Which system operations may it use? +- Where does it appear in the product? +- What proves that it works? -```sh -npm test -npm run demo +That same contract guides product, engineering, testing, and AI implementation. + +```text +Business need + ↓ +Clear feature contract + ↓ +Human or AI implementation + ↓ +Automated boundary + behavior checks + ↓ +Reviewable product change ``` -Expected demo output: +## Humans and AI work as partners -```json -{ - "ok": true, - "value": { - "projectId": "project-1", - "archived": true - } -} -``` +AIFA is not designed to replace human judgment. It separates the work so each side can contribute where it is strongest. -## What Makes A Feature Atomic? +### Humans provide direction and judgment -An AIFA feature should: +- choose the customer problem and desired outcome; +- define business rules, risk, permissions, and tradeoffs; +- decide whether a feature contract is complete; +- review user experience and business impact; +- approve changes that affect architecture or policy. -1. Have one responsibility. -2. Declare its input and output. -3. Declare the capabilities it needs. -4. Use only the provided context. -5. Return explicit success or failure results. -6. Be testable without the real application. +### AI accelerates bounded implementation -## Why Ordinary Small Tickets Are Not Enough +- reads the feature contract and relevant local rules; +- implements backend and frontend work inside the declared boundary; +- writes or updates focused tests; +- checks schemas, dependencies, and acceptance criteria; +- explains what changed for human review. -A ticket can look small while still hiding a lot of context. +### The architecture keeps both aligned -```md -Allow users to archive a project. -``` +The runtime blocks undeclared system access. Automated checks reject invalid contracts and forbidden dependencies. Humans can therefore review a smaller, more explicit change instead of trusting that an AI agent discovered every hidden convention. + +## Real-world example: Focusly -To implement that safely, a developer or AI agent may still need to discover: +[Focusly](./demo/ai-todo-assistant/README.md) is a production-style AI task workspace. People can create and manage tasks, while AI can propose a plan and use approved operations through the same rules as the browser application. -- where project logic lives -- how permissions work -- whether archived projects are soft-deleted -- whether audit events are required -- which persistence abstraction is used -- what response shape the app expects -- which tests prove the behavior +It demonstrates that AIFA can support a real full-stack product: -AIFA tries to make the hidden context explicit. +- task creation, listing, status changes, and deletion; +- reviewable AI-generated task plans; +- browser and MCP access through the same feature contracts; +- tenant isolation, authorization, idempotency, auditing, and concurrency safety; +- dynamically composed React screens; +- automated architecture and behavior checks. -## Example AIFA Ticket +# Part II — How it works technically -```md -# Feature: Archive Project +## The core idea -## Goal +Each product use case lives in one feature slice: + +```text +contexts//features// +``` -Allow an authorized actor to archive one project. +The slice keeps its business description, data contracts, backend behavior, frontend contribution, and implementation plan together: -## Input +| File or folder | Responsibility | +| --- | --- | +| `feature.definition.json` | Business need, route, capabilities, security, events, UI slots, and work items | +| `contracts/` | Valid input and output shapes | +| `backend/` | Business behavior without direct infrastructure access | +| `frontend/` | Feature-owned UI and hooks | +| `manifest.ts` | Connects the feature to backend, frontend, and MCP discovery | +| `IMPLEMENTATION_PLAN.md` | Human-readable implementation and acceptance guide | -- projectId: string - The project to archive. +Core discovers these slices automatically. Ordinary product features do not need to be added to a central registry. -## Output +## Short example 1: declare the contract -- projectId: string - The archived project id. -- archived: boolean - Always true on success. +This shortened definition says why Create Task exists and where it connects: -## Allowed Capabilities +```json +{ + "name": "create-task", + "businessNeed": "Capture a categorized commitment from a person or AI suggestion.", + "backend": { + "route": "/api/tasks", + "capabilities": ["TaskCreate", "IdCreate", "ClockNow"] + }, + "frontend": { "slots": ["TaskComposer"] }, + "security": { "actorRequired": true, "idempotent": true } +} +``` -- project.load - Load the project by id. -- project.save - Persist the archived project. -- permission.check - Confirm the actor can archive the project. -- audit.record - Record the archival action. +The contract is readable by people and validated by tools. -## Rules +## Short example 2: implement backend behavior -- The project must exist. -- The actor must have project.archive. -- The actor must belong to the same organization as the project. -- Archiving an already archived project succeeds. +The backend feature receives a context. It does not import MongoDB, authentication code, or an HTTP framework: -## Failure Cases +```ts +async execute(context) { + const title = context.input.title.trim(); -- not_found - The project does not exist. -- not_allowed - The actor cannot archive the project. + if (!title) { + return context.fail("InvalidInput", "Task title is required"); + } -## Acceptance Tests + const task = await context.capabilities.TaskCreate({ + task: { title, status: "Todo" } + }); -- Archives an active project. -- Returns success for an already archived project. -- Rejects an actor without archive permission. -- Returns not_found for a missing project. + return context.ok({ task }); +} ``` -This ticket is not just a task description. It is a contract. +The runtime supplies only the capabilities declared by the feature. If the feature tries to use something else, execution fails clearly. -The AI agent does not need to inspect the whole codebase to discover the feature boundary. The boundary is already written down. +### Backend flow -## Example Implementation +```text +Browser request or AI tool + → authenticate and authorize + → validate input + → execute the same AIFA feature + → grant only declared capabilities + → database / AI provider / audit / outbox +``` -The matching feature only uses the context and the capabilities declared in the ticket. +This is why a browser user and an AI client cannot silently follow different business rules. -```js -export const archiveProject = defineFeature({ - name: "archive-project", - description: "Archive one project when the actor has permission.", - input: { - projectId: "string", - }, - output: { - projectId: "string", - archived: "boolean", - }, - capabilities: ["project.load", "project.save", "permission.check", "audit.record"], - - async execute(context) { - const project = await context.capabilities["project.load"]({ - projectId: context.input.projectId, - }); - - if (!project) { - return context.fail("not_found", "Project was not found", { - projectId: context.input.projectId, - }); - } - - const allowed = await context.capabilities["permission.check"]({ - actor: context.actor, - permission: "project.archive", - resource: project, - }); - - if (!allowed) { - return context.fail("not_allowed", "Actor cannot archive this project", { - projectId: project.id, - }); - } - - if (!project.archived) { - await context.capabilities["project.save"]({ - project: { - ...project, - archived: true, - }, - }); - } - - await context.capabilities["audit.record"]({ - type: "project.archived", - actorId: context.actor.id, - projectId: project.id, - }); - - return context.ok({ - projectId: project.id, - archived: true, - }); - }, -}); +## Short example 3: contribute frontend UI + +The React application provides named, typed places called slots. A feature contributes its own UI without editing a central screen: + +```ts +export const contribution = { + slot: "TaskComposer", + name: "create-task-form", + render: () => +}; ``` -Notice what is missing: +The feature-local form calls the same Create Task contract used by other clients. After success, semantic cache tags update every view that depends on the task collection. -- no database import -- no framework import -- no service container -- no global config -- no hidden repository convention -- no unrelated project knowledge +### Frontend flow -The runtime owns infrastructure. The feature owns behavior. +```text +React application shell + → discover feature contributions + → render them in typed slots + → call feature contracts through local hooks + → update shared server state through semantic tags +``` + +Features do not directly import or refresh one another. That keeps screens composable as the application grows. -## Why This Is Useful +## What enforces the architecture? -AIFA is useful because it changes what needs to be understood before work can start. +AIFA uses executable checks rather than relying on documentation alone: -Traditional feature work: +- JSON Schema validates feature definitions and boundary data; +- generated TypeScript types keep contracts aligned with code; +- dependency checks prevent features from importing forbidden internals; +- the runtime rejects undeclared capabilities; +- behavioral tests execute features through the real runtime boundary; +- integration tests cover authorization, tenancy, idempotency, concurrency, HTTP, and MCP behavior. -```txt -Read ticket -Explore codebase -Infer architecture -Find dependencies -Guess conventions -Implement -Patch tests -Review for accidental coupling +## Repository guide + +```text +. +├── README.md # Business and technical overview +├── docs/ # Architecture docs and GitHub Pages website +└── demo/ai-todo-assistant/ + ├── README.md # Focusly product and local setup + ├── ARCHITECTURE.md # Authoritative technical architecture + ├── AGENTS.md # Contributor rules + ├── core/ # Runtime and application composition + ├── contexts/ # Business areas and feature slices + └── tests/ # Architecture, behavior, and integration checks ``` -AIFA feature work: +## Read next + +| Goal | Document | +| --- | --- | +| Understand the architecture | [AIFA architecture guide](./docs/architecture.md) | +| Explore the working product | [Focusly demo](./demo/ai-todo-assistant/README.md) | +| Make an implementation decision | [Authoritative demo architecture](./demo/ai-todo-assistant/ARCHITECTURE.md) | +| Add or change a feature | [Contributor working agreement](./demo/ai-todo-assistant/AGENTS.md) | +| Write a feature brief | [AIFA feature contract](./docs/ticket-contract.md) | +| View the presentation | [GitHub Pages website](https://adrianbesleaga.github.io/AIFA/) | -```txt -Read ticket contract -Read runtime contract -Implement one feature -Run behavioral tests +## Run Focusly + +Requirements: Node.js 20+, Docker, and Docker Compose. + +```bash +cd demo/ai-todo-assistant +npm install +cp .env.example .env +docker compose up -d +npm run dev ``` -This makes small tasks more suitable for: +Verify the application with: -- AI coding agents -- new contributors -- parallel development -- focused code review -- safer refactoring -- implementation from specs +```bash +npm run check +npm test +npm run build +``` -## Why This Matters +## The rule to remember -This architecture optimizes for delegatable work. A small ticket can be handed to a human developer or an AI agent with a clear contract and minimal hidden context. +> A feature knows its purpose, its contract, and its allowed capabilities—not the whole application. -The long-term goal is not just smaller tasks. It is software that is designed so small tasks are safe, understandable, testable, and composable by default. +That makes the work easier to understand, safer to delegate, and simpler for humans and AI to review together. diff --git a/demo/ai-todo-assistant/README.md b/demo/ai-todo-assistant/README.md index 36459b5..150e4c3 100644 --- a/demo/ai-todo-assistant/README.md +++ b/demo/ai-todo-assistant/README.md @@ -2,6 +2,8 @@ Focusly is an AI-assisted task workspace built as a production-style demonstration of **AI Atomic Feature Architecture (AIFA)**. It helps people turn a natural-language goal into a practical task plan, review the suggestions, and move approved work through a focused Kanban workflow. +> New to the repository? Start with the [AIFA overview](../../README.md), then use this page to run the application. The [full architecture](./ARCHITECTURE.md) is authoritative for implementation decisions. + ![Focusly landing page](./public/readme-preview.jpg) ## The workspace @@ -84,7 +86,7 @@ contexts/ tests/ Runtime, boundary, architecture, and integration coverage ``` -See [ARCHITECTURE.md](./ARCHITECTURE.md) for the full architectural contract and [BUSINESS.md](./BUSINESS.md) for the product rationale. +See [ARCHITECTURE.md](./ARCHITECTURE.md) for the full architectural contract, [AGENTS.md](./AGENTS.md) for the contributor working agreement, [BUSINESS.md](./BUSINESS.md) for the product rationale, and the concise [AIFA architecture guide](../../docs/architecture.md) for the conceptual model. ## Run locally diff --git a/docs/architecture.md b/docs/architecture.md index 0330fc9..0784fec 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -1,107 +1,156 @@ -# Architecture +# AIFA Architecture + +AI Atomic Feature Architecture (AIFA) organizes an application around small feature contracts instead of framework layers. A feature owns one product behavior; the runtime owns infrastructure and composition. + +This document explains the model. The [Focusly architecture](../demo/ai-todo-assistant/ARCHITECTURE.md) is the authoritative description of the production-style implementation in this repository. + +## The design goal + +A contributor should be able to implement or change one feature by reading its contract, its bounded-context rules, and the runtime interface—not the whole codebase. + +```text +Feature definition + schemas + │ + ▼ + Discovered manifest + │ + ┌──────┴──────┐ + ▼ ▼ + HTTP / MCP React UI slot + │ │ + └──────┬──────┘ + ▼ + AIFA runtime + │ + ▼ + Granted capabilities → infrastructure +``` -AI Atomic Feature Architecture separates feature behavior from application infrastructure so small tasks can be implemented by AI agents or humans without full project context. +## The three boundaries -## Core Concepts +### 1. Feature contract -### Feature +A feature declares its: -A feature is the smallest meaningful implementation unit. It performs one user-visible or domain-visible behavior. +- business purpose and bounded context; +- input, output, and typed failures; +- required runtime capabilities; +- HTTP route and optional MCP tool; +- frontend slot contributions; +- security, idempotency, confirmation, and event requirements; +- acceptance criteria and tests. -A feature must define: +In the demo, `feature.definition.json` is the machine-readable planning contract, JSON Schemas define boundary data, and `manifest.ts` binds the declaration to executable backend and frontend code. -- `name` -- `description` -- `input` -- `output` -- `capabilities` -- `execute(context)` +### 2. Runtime context -### Context +The runtime constructs the complete world visible to backend feature logic: -The context is the complete world visible to the feature. +- `input` — validated feature input; +- `actor` — authenticated user or system identity; +- `metadata` — request and execution metadata; +- `capabilities` — only the operations declared by the feature; +- `ok` and `fail` — structured result helpers. -It contains: +Capabilities are narrow permissions such as saving a task, generating a plan, or recording an audit event. They are not database clients, HTTP objects, SDKs, or general-purpose service containers. -- `input`: validated feature input -- `actor`: the user or system performing the action -- `metadata`: request or execution metadata -- `capabilities`: runtime-provided operations -- `ok(value)`: success result helper -- `fail(code, message, details)`: failure result helper +### 3. Bounded context -### Capability +Related business language and invariants live together under `contexts//`. Feature slices may use their own context’s domain rules, but they may not import another feature or another context’s internal domain. Cross-context communication uses versioned contracts or domain events. -A capability is an operation the runtime permits a feature to perform. +## Backend flow -Examples: +Both browser requests and AI clients use the same feature boundary: -- `project.load` -- `project.save` -- `permission.check` -- `audit.record` -- `event.publish` -- `email.send` +```text +HTTP request or MCP tool + → dynamic discovery + → authentication and authorization + → input schema validation + → idempotency and transaction boundary + → feature execution + → declared capabilities + → MongoDB, AI provider, audit, and outbox adapters +``` -Capabilities are not repositories, services, SDKs, or framework objects. They are narrow permissions to perform explicit actions. +The feature contains business behavior but has no direct infrastructure imports. The runtime can therefore enforce tenant scope, permissions, concurrency, auditing, and capability access consistently regardless of entry point. -### Runtime +### Backend rules -The runtime owns infrastructure. +1. A feature uses only values on its runtime context. +2. Infrastructure is accessed only through declared capabilities. +3. HTTP and MCP adapters contain translation, not business logic. +4. Routes and tools invoke the same discovered feature implementation. +5. Mutations declare authorization, idempotency, confirmation, and concurrency behavior. +6. Cross-context work uses public, versioned contracts or events. -It is responsible for: +## Frontend flow -- validating feature contracts -- constructing context objects -- exposing allowed capabilities -- hiding infrastructure details -- enforcing capability boundaries -- recording execution metadata +The frontend applies the same composition model: -### Ticket Contract +```text +React app shell + → discovers feature manifests + → registers typed contributions + → renders named UI slots + → feature-local hooks call HTTP contracts + → shared query runtime caches server state + → semantic tags and domain events invalidate affected data +``` -The ticket contract is the human-facing version of the feature contract. It should provide enough information for implementation without requiring broad project knowledge. +The app shell owns layout, authentication-aware transport, shared query infrastructure, event transport, theme, and slot definitions. Each feature owns the component and hooks for its behavior. -## Dependency Direction +A Create Task contribution does not import the task list or tell it to refresh. After success it invalidates a stable Task Collection tag. Every independent contribution that reads that resource updates through the generic query runtime. -AIFA inverts the common dependency shape. +### Frontend rules -Traditional shape: +1. Feature UI contributes through named, typed slots. +2. A feature does not import another feature’s component or hook. +3. Components own transient presentation state; the query runtime owns server state. +4. Feature-local hooks adapt one feature contract to React. +5. Synchronization uses semantic resource tags and versioned domain events, not component-to-component commands. +6. Core provides generic composition and never contains product behavior. -```txt -Feature -> Service -> Repository -> Database +## Dependency direction + +```text +core → core only +context domain → core + its own domain +feature → core + itself + own domain + versioned contracts +HTTP / MCP / UI adapter → core + its registered feature contract +cross-context work → versioned contracts or domain events ``` -AIFA shape: +Forbidden dependencies include: -```txt -Feature -> Context Capability -> Runtime -> Infrastructure -``` +- core importing a product context or feature; +- one feature importing another feature; +- one context importing another context’s internal domain; +- features importing MongoDB, HTTP, MCP, environment, or AI-provider SDKs; +- circular imports or hand-maintained central feature registries. -The feature knows the operation it is allowed to request. It does not know the implementation. +## How the boundaries are enforced -## Design Rules +AIFA relies on executable checks, not documentation alone: -1. Features do not import runtime infrastructure. -2. Features do not create database clients. -3. Features do not call external APIs directly. -4. Features do not read global config. -5. Features do not mutate shared global state. -6. Features only use declared capabilities. -7. Features return structured results. +- JSON Schema validation for feature definitions and boundary data; +- generated TypeScript types from schemas; +- deterministic dynamic discovery with duplicate detection; +- dependency graph and import-boundary validation; +- runtime denial of undeclared capabilities; +- behavioral tests through the AIFA runtime; +- HTTP/MCP parity, authorization, tenancy, concurrency, and integration tests. -## Intended Benefits +## Tradeoffs -- Smaller implementation units -- Less hidden context -- Easier onboarding -- Safer AI-generated changes -- Focused tests -- Easier review -- Lower coupling between feature code and infrastructure +AIFA moves design work toward contracts, capabilities, and composition. Poorly chosen capabilities can become noisy or too broad, and not every line of code should become its own feature. The useful unit is the smallest meaningful product behavior that can be specified, implemented, and verified independently. -## Known Tradeoffs +The architecture pays off when a growing system needs parallel work, clear review boundaries, multiple entry points, or safe delegation to AI agents. -AIFA moves complexity upstream. Someone still has to design the runtime, capabilities, and ticket contracts carefully. +## Explore the implementation -It can also become restrictive if the capability model is too narrow or noisy. The goal is not to make every line of code isolated. The goal is to make feature implementation isolated enough to be safely delegated. +- [Focusly product and setup](../demo/ai-todo-assistant/README.md) +- [Full implementation architecture](../demo/ai-todo-assistant/ARCHITECTURE.md) +- [Contributor working agreement](../demo/ai-todo-assistant/AGENTS.md) +- [Feature ticket contract](./ticket-contract.md) +- [Repository overview](../README.md) diff --git a/docs/index.html b/docs/index.html index 0fb4e2f..6447fcf 100644 --- a/docs/index.html +++ b/docs/index.html @@ -3,275 +3,442 @@ - AI Atomic Feature Architecture + AIFA — Clearer software delivery for humans and AI - + + - + + + -
      -
      -
      F -

      Composition boundary

      Frontend

      +
      +

      Composition boundary

      +

      Frontend

      +

      - The React shell owns typed slots and shared runtime services. Features contribute UI - without reaching into each other or the shell. + The React shell owns typed slots and shared runtime services. + Features contribute UI without reaching into each other or the + shell.

      1. 01 Discover frontend manifest
      2. @@ -328,7 +526,9 @@

        Clear on the backend. Composable on the frontend.03 Call feature through local hook
      3. 04 Invalidate semantic cache tags
      -

      No feature-to-feature component imports or refresh commands.

      +

      + No feature-to-feature component imports or refresh commands. +

      @@ -348,13 +548,20 @@

      Clear on the backend. Composable on the frontend.One idea, expressed at each boundary.

      - These shortened examples come from Focusly’s Create Task feature. The business need - stays recognizable all the way from declaration to UI. + These shortened examples come from Focusly’s Create Task feature. + The business need stays recognizable all the way from declaration + to UI.

      -
      01
      Declare

      The contract

      +
      + 01 +
      + Declare +

      The contract

      +
      +
      {
         "name": "create-task",
         "capabilities": [
      @@ -366,7 +573,13 @@ 

      One idea, expressed at each boundary.

      States what the feature may do and where it appears.

      -
      02
      Backend

      The behavior

      +
      + 02 +
      + Backend +

      The behavior

      +
      +
      async execute(context) {
         const task = await
           context.capabilities.TaskCreate({
      @@ -381,7 +594,13 @@ 

      One idea, expressed at each boundary.

      Uses only the system operations granted by the runtime.

      -
      03
      Frontend

      The contribution

      +
      + 03 +
      + Frontend +

      The contribution

      +
      +
      export const contribution = {
         slot: "TaskComposer",
         name: "create-task-form",
      @@ -393,27 +612,61 @@ 

      One idea, expressed at each boundary.

      -
      +

      From idea to implementation.

      -

      Start with the overview, inspect the working app, then use the detailed contract when contributing.

      +

      + Start with the overview, inspect the working app, then use the + detailed contract when contributing. +

      @@ -422,10 +675,14 @@

      From idea to implementation.

      See the boundaries in working code.

      -

      Clone the repository, enter the demo, and start Focusly locally.

      +

      + Clone the repository, enter the demo, and start Focusly locally. +

      -
      terminal
      +
      + terminal +
      $ cd demo/ai-todo-assistant
       $ npm install
       $ cp .env.example .env
      @@ -436,9 +693,17 @@ 

      See the boundaries in working code.

      diff --git a/docs/script.js b/docs/script.js index 05ae2df..a2c053b 100644 --- a/docs/script.js +++ b/docs/script.js @@ -1,6 +1,7 @@ const canvas = document.querySelector("#aifa-canvas"); const context = canvas.getContext("2d"); -const systemMap = document.querySelector(".system-map"); +const siteHeader = document.querySelector(".site-header"); +const navLinks = [...document.querySelectorAll("nav a[href^='#']")]; const prefersReducedMotion = window.matchMedia("(prefers-reduced-motion: reduce)"); let width = 0; @@ -137,11 +138,35 @@ window.addEventListener("resize", start); window.addEventListener("pointermove", (event) => { pointerX = event.clientX / window.innerWidth - 0.5; pointerY = event.clientY / window.innerHeight - 0.5; +}); - if (systemMap) { - systemMap.style.setProperty("--map-x", `${pointerX * -10}px`); - systemMap.style.setProperty("--map-y", `${pointerY * -8}px`); - } +function updateScrollState() { + const scrollRange = document.documentElement.scrollHeight - window.innerHeight; + const progress = scrollRange > 0 ? (window.scrollY / scrollRange) * 100 : 0; + siteHeader?.style.setProperty("--scroll-progress", `${Math.min(progress, 100)}%`); +} + +const sectionObserver = new IntersectionObserver( + (entries) => { + const visible = entries + .filter((entry) => entry.isIntersecting) + .sort((a, b) => b.intersectionRatio - a.intersectionRatio)[0]; + + if (!visible) return; + + navLinks.forEach((link) => { + link.classList.toggle("active", link.getAttribute("href") === `#${visible.target.id}`); + }); + }, + { rootMargin: "-30% 0px -58%", threshold: [0, 0.15, 0.4] }, +); + +navLinks.forEach((link) => { + const section = document.querySelector(link.getAttribute("href")); + if (section) sectionObserver.observe(section); }); +window.addEventListener("scroll", updateScrollState, { passive: true }); + start(); +updateScrollState(); diff --git a/docs/styles.css b/docs/styles.css index b4bb2a6..7a4144f 100644 --- a/docs/styles.css +++ b/docs/styles.css @@ -1,19 +1,22 @@ :root { - --ink: #102019; - --muted: #65756e; - --paper: #f7f8f3; - --surface: #ffffff; - --dark: #0b1713; - --dark-soft: #12251e; - --line: #dce5de; + --void: #050b09; + --deep: #08130f; + --panel: #0d1d17; + --panel-2: #11251d; + --ink: #f3faf6; + --muted: #91a79d; + --dim: #5f756b; + --line: rgba(146, 202, 180, 0.14); + --line-bright: rgba(105, 230, 184, 0.34); --mint: #69e6b8; - --mint-deep: #128460; + --mint-hot: #a1ffda; + --teal: #25ad82; + --violet: #aa92ff; --coral: #ff7668; --gold: #f5c65b; - --violet: #a992f5; - --blue: #62aee8; - --shadow: 0 26px 80px rgba(15, 35, 27, 0.13); - --radius: 18px; + --blue: #62b5ef; + --radius: 20px; + --shadow: 0 30px 90px rgba(0, 0, 0, 0.38); } * { @@ -25,14 +28,32 @@ html { } body { - margin: 0; - background: var(--paper); + background: var(--void); color: var(--ink); font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; line-height: 1.6; + margin: 0; overflow-x: hidden; } +body::before { + background-image: + linear-gradient(rgba(105, 230, 184, 0.018) 1px, transparent 1px), + linear-gradient(90deg, rgba(105, 230, 184, 0.018) 1px, transparent 1px); + background-size: 64px 64px; + content: ""; + inset: 0; + mask-image: linear-gradient(180deg, #000, transparent 80%); + pointer-events: none; + position: fixed; + z-index: -1; +} + +::selection { + background: var(--mint); + color: var(--void); +} + a { color: inherit; text-decoration: none; @@ -44,52 +65,100 @@ pre { font: inherit; } +h1, +h2, +h3, +p { + margin-top: 0; +} + +h1 { + font-size: clamp(52px, 5.7vw, 84px); + letter-spacing: -0.065em; + line-height: 0.94; + margin-bottom: 28px; +} + +h2 { + font-size: clamp(38px, 4.35vw, 62px); + letter-spacing: -0.055em; + line-height: 1.02; + margin-bottom: 20px; +} + +h3 { + font-size: 21px; + letter-spacing: -0.025em; + line-height: 1.18; + margin-bottom: 10px; +} + .skip-link { background: var(--mint); - color: var(--dark); - font-weight: 800; + border-radius: 8px; + color: var(--void); + font-weight: 850; left: 16px; padding: 10px 14px; position: fixed; - top: -80px; + top: -90px; z-index: 100; } .skip-link:focus { - top: 12px; + top: 14px; } .site-header { align-items: center; - backdrop-filter: blur(16px); - background: rgba(247, 248, 243, 0.9); - border-bottom: 1px solid rgba(220, 229, 222, 0.85); + backdrop-filter: blur(24px); + background: rgba(6, 14, 11, 0.82); + border: 1px solid rgba(105, 230, 184, 0.14); + border-radius: 16px; + box-shadow: 0 16px 50px rgba(0, 0, 0, 0.24); display: grid; grid-template-columns: 1fr auto 1fr; - height: 72px; - padding: 0 clamp(20px, 4vw, 64px); - position: sticky; - top: 0; - z-index: 20; + height: 68px; + left: 20px; + margin: 0 auto; + max-width: 1380px; + padding: 0 14px 0 18px; + position: fixed; + right: 20px; + top: 14px; + z-index: 30; +} + +.site-header::after { + background: linear-gradient(90deg, var(--mint), var(--violet)); + bottom: -1px; + content: ""; + height: 1px; + left: 18px; + position: absolute; + transition: width 80ms linear; + width: var(--scroll-progress, 0%); } .brand { align-items: center; display: flex; - font-size: 17px; + font-size: 15px; font-weight: 900; gap: 10px; justify-self: start; - letter-spacing: -0.02em; + letter-spacing: 0.03em; } .brand-mark { align-items: center; - background: var(--dark); + background: linear-gradient(145deg, var(--mint-hot), var(--teal)); border-radius: 9px; - color: var(--mint); + box-shadow: 0 0 28px rgba(105, 230, 184, 0.22); + color: var(--void); display: inline-flex; - font-weight: 900; + font-family: "SFMono-Regular", Consolas, monospace; + font-weight: 950; height: 34px; justify-content: center; width: 34px; @@ -98,52 +167,65 @@ pre { nav { align-items: center; display: flex; - gap: clamp(18px, 3vw, 34px); + gap: clamp(20px, 2.7vw, 38px); } nav a { - color: #52635b; - font-size: 14px; - font-weight: 750; + color: rgba(242, 250, 246, 0.54); + font-size: 12px; + font-weight: 800; + letter-spacing: 0.03em; transition: color 160ms ease; } nav a:hover, -nav a:focus-visible { - color: var(--mint-deep); +nav a:focus-visible, +nav a.active { + color: var(--mint); } .header-cta { - background: var(--dark); - border-radius: 9px; - color: white; - font-size: 13px; - font-weight: 800; + background: var(--mint); + border-radius: 10px; + box-shadow: 0 10px 32px rgba(39, 170, 122, 0.18); + color: var(--void); + font-size: 12px; + font-weight: 900; justify-self: end; - padding: 10px 15px; + padding: 11px 15px; + transition: box-shadow 160ms ease, transform 160ms ease; +} + +.header-cta:hover, +.header-cta:focus-visible { + box-shadow: 0 14px 40px rgba(105, 230, 184, 0.25); + transform: translateY(-1px); } .hero { - background: var(--dark); - color: white; - min-height: calc(100vh - 72px); + background: + radial-gradient(circle at 77% 42%, rgba(49, 190, 139, 0.14), transparent 28%), + radial-gradient(circle at 12% 80%, rgba(123, 92, 228, 0.1), transparent 30%), + linear-gradient(135deg, #050c09 0%, #0a1812 52%, #06100c 100%); + min-height: 100vh; overflow: hidden; position: relative; } .hero::after { - background: linear-gradient(90deg, rgba(105, 230, 184, 0.7), var(--coral), var(--violet)); + background: linear-gradient(90deg, transparent, var(--mint), var(--violet), transparent); bottom: 0; content: ""; - height: 3px; + height: 1px; left: 0; + opacity: 0.75; position: absolute; right: 0; } .hero-ambient { inset: 0; - opacity: 0.85; + opacity: 0.5; pointer-events: none; position: absolute; } @@ -151,113 +233,112 @@ nav a:focus-visible { #aifa-canvas { height: 100%; inset: 0; + mask-image: linear-gradient(90deg, transparent 0%, transparent 42%, rgba(0, 0, 0, 0.32) 58%, #000 100%); + -webkit-mask-image: linear-gradient(90deg, transparent 0%, transparent 42%, rgba(0, 0, 0, 0.32) 58%, #000 100%); position: absolute; width: 100%; } -.system-map { - height: 520px; - opacity: 0.55; +.ambient-orbit { + border: 1px solid rgba(105, 230, 184, 0.1); + border-radius: 50%; position: absolute; - right: -100px; + right: -12vw; top: 50%; - transform: translateY(-50%) translate(var(--map-x, 0), var(--map-y, 0)); - width: min(50vw, 720px); + transform: translateY(-50%); } -.system-node { - animation: drift 6s ease-in-out infinite; - background: rgba(11, 23, 19, 0.74); - border: 1px solid rgba(105, 230, 184, 0.34); - border-radius: 10px; - box-shadow: 0 0 30px rgba(105, 230, 184, 0.08); - color: rgba(255, 255, 255, 0.72); - font-family: "SFMono-Regular", Consolas, monospace; - font-size: 11px; - font-weight: 800; - padding: 10px 13px; +.ambient-orbit::before, +.ambient-orbit::after { + border: 1px solid rgba(169, 146, 245, 0.07); + border-radius: inherit; + content: ""; + inset: 11%; position: absolute; - text-transform: uppercase; } -.node-definition { left: 6%; top: 18%; } -.node-manifest { left: 12%; top: 68%; animation-delay: -1s; } -.node-feature { left: 47%; top: 42%; animation-delay: -2s; border-color: var(--mint); color: white; } -.node-slot { right: 3%; top: 17%; animation-delay: -3s; } -.node-runtime { right: 5%; top: 72%; animation-delay: -4s; } +.ambient-orbit::after { + border-color: rgba(105, 230, 184, 0.05); + inset: 24%; +} + +.orbit-one { + height: 72vw; + max-height: 1040px; + max-width: 1040px; + width: 72vw; +} + +.orbit-two { + height: 48vw; + max-height: 700px; + max-width: 700px; + right: -1vw; + transform: translateY(-50%) rotate(28deg); + width: 48vw; +} .hero-inner { align-items: center; display: grid; - gap: clamp(50px, 7vw, 110px); - grid-template-columns: minmax(0, 1.06fr) minmax(410px, 0.94fr); + gap: clamp(54px, 6vw, 96px); + grid-template-columns: minmax(0, 0.96fr) minmax(480px, 1.04fr); margin: 0 auto; - max-width: 1320px; - min-height: calc(100vh - 72px); - padding: 96px clamp(24px, 5vw, 72px); + max-width: 1380px; + min-height: 100vh; + padding: 132px clamp(24px, 5vw, 72px) 78px; position: relative; z-index: 2; } .hero-copy { - max-width: 700px; + max-width: 680px; } .eyebrow, .section-label { - color: var(--mint-deep); - font-size: 12px; + color: var(--mint); + font-size: 10px; font-weight: 900; - letter-spacing: 0.12em; - margin: 0 0 16px; + letter-spacing: 0.16em; + margin: 0 0 17px; text-transform: uppercase; } -.eyebrow, -.section-label.light { - color: var(--mint); -} - -h1, -h2, -h3, -p { - margin-top: 0; -} - -h1 { - font-size: clamp(54px, 6.4vw, 94px); - letter-spacing: -0.065em; - line-height: 0.93; - margin-bottom: 28px; +.hero .eyebrow { + align-items: center; + display: flex; + gap: 10px; } -h2 { - font-size: clamp(36px, 4.5vw, 62px); - letter-spacing: -0.05em; - line-height: 1.02; - margin-bottom: 20px; +.hero .eyebrow span { + background: var(--mint); + border-radius: 50%; + box-shadow: 0 0 0 5px rgba(105, 230, 184, 0.07), 0 0 18px var(--mint); + height: 6px; + width: 6px; } -h3 { - font-size: 22px; - letter-spacing: -0.025em; - line-height: 1.2; - margin-bottom: 10px; +.hero h1 em { + background: linear-gradient(100deg, #fff 8%, #75e8bd 55%, #b8a6ff 100%); + -webkit-background-clip: text; + background-clip: text; + color: transparent; + font-style: normal; } .hero-lede { - color: rgba(255, 255, 255, 0.7); - font-size: clamp(19px, 1.65vw, 23px); - line-height: 1.48; + color: rgba(229, 242, 235, 0.66); + font-size: clamp(18px, 1.55vw, 22px); + line-height: 1.5; margin-bottom: 34px; - max-width: 660px; + max-width: 640px; } .hero-actions { display: flex; flex-wrap: wrap; - gap: 12px; + gap: 11px; } .button { @@ -265,55 +346,52 @@ h3 { border: 1px solid transparent; border-radius: 10px; display: inline-flex; - font-size: 14px; - font-weight: 850; + font-size: 12px; + font-weight: 900; gap: 12px; justify-content: center; - min-height: 50px; + min-height: 49px; padding: 13px 18px; - transition: transform 160ms ease, box-shadow 160ms ease, background 160ms ease; + transition: box-shadow 160ms ease, transform 160ms ease, background 160ms ease; } .button:hover, .button:focus-visible { - box-shadow: 0 15px 34px rgba(0, 0, 0, 0.18); + box-shadow: 0 14px 38px rgba(0, 0, 0, 0.28); transform: translateY(-2px); } .button-primary { background: var(--mint); - color: var(--dark); + color: var(--void); } .button-ghost { - border-color: rgba(255, 255, 255, 0.2); - color: white; -} - -.button-ghost:hover { - background: rgba(255, 255, 255, 0.06); + background: rgba(255, 255, 255, 0.025); + border-color: rgba(255, 255, 255, 0.14); + color: var(--ink); } .button-light { - background: white; - color: var(--dark); + background: var(--mint); + color: var(--void); } .signal-list { display: flex; flex-wrap: wrap; - gap: 22px; + gap: 20px; list-style: none; margin: 34px 0 0; padding: 0; } .signal-list li { - color: rgba(255, 255, 255, 0.58); - font-size: 12px; - font-weight: 800; - letter-spacing: 0.04em; - padding-left: 15px; + color: rgba(228, 242, 235, 0.42); + font-size: 9px; + font-weight: 850; + letter-spacing: 0.09em; + padding-left: 14px; position: relative; text-transform: uppercase; } @@ -321,820 +399,1024 @@ h3 { .signal-list li::before { background: var(--mint); border-radius: 50%; + box-shadow: 0 0 10px rgba(105, 230, 184, 0.5); content: ""; - height: 6px; + height: 5px; left: 0; position: absolute; - top: 7px; - width: 6px; -} - -.hero-proof, -.terminal { - background: rgba(16, 32, 25, 0.94); - border: 1px solid rgba(105, 230, 184, 0.18); - border-radius: var(--radius); - box-shadow: 0 38px 100px rgba(0, 0, 0, 0.35); - overflow: hidden; + top: 5px; + width: 5px; } -.outcome-panel { - background: rgba(16, 32, 25, 0.94); - border: 1px solid rgba(105, 230, 184, 0.2); - border-radius: var(--radius); - box-shadow: 0 38px 100px rgba(0, 0, 0, 0.35); +.future-console { + background: linear-gradient(155deg, #10251d 0%, #091712 58%, #0e2019 100%); + border: 1px solid rgba(105, 230, 184, 0.27); + border-radius: 24px; + box-shadow: 0 32px 100px rgba(0, 0, 0, 0.52), 0 0 80px rgba(38, 171, 122, 0.08); + isolation: isolate; + justify-self: end; + max-width: 620px; overflow: hidden; - padding: 34px; - transform: rotate(1deg); -} - -.outcome-eyebrow { - color: var(--mint); - font-size: 11px; - font-weight: 900; - letter-spacing: 0.12em; - margin-bottom: 10px; - text-transform: uppercase; + position: relative; + width: 100%; } -.outcome-panel h2 { - font-size: clamp(30px, 3vw, 44px); - margin-bottom: 28px; +.future-console::before { + background: + linear-gradient(90deg, transparent 49.5%, rgba(105, 230, 184, 0.035) 50%, transparent 50.5%), + linear-gradient(transparent 49.5%, rgba(105, 230, 184, 0.035) 50%, transparent 50.5%); + background-size: 44px 44px; + content: ""; + inset: 0; + opacity: 0.52; + pointer-events: none; + position: absolute; + z-index: -1; } -.outcome-steps { - display: grid; - gap: 1px; +.future-console::after { + background: linear-gradient(90deg, var(--mint), var(--violet), var(--coral)); + content: ""; + height: 2px; + left: 28px; + opacity: 0.8; + position: absolute; + right: 28px; + top: 0; } -.outcome-steps div { +.console-header { align-items: center; - background: rgba(255, 255, 255, 0.035); - border-bottom: 1px solid rgba(255, 255, 255, 0.07); - display: grid; - gap: 12px; - grid-template-columns: 30px 1fr; - padding: 15px 13px; + border-bottom: 1px solid rgba(255, 255, 255, 0.075); + display: flex; + justify-content: space-between; + min-height: 68px; + padding: 0 24px; } -.outcome-steps div:last-child { - border-bottom: 0; +.console-header > div { + align-items: center; + display: flex; + gap: 11px; } -.outcome-steps span { - color: var(--coral); +.console-mark { + align-items: center; + background: var(--mint); + border-radius: 7px; + box-shadow: 0 0 22px rgba(105, 230, 184, 0.28); + color: var(--void); + display: flex; font-family: "SFMono-Regular", Consolas, monospace; - font-size: 10px; - font-weight: 900; - grid-row: 1 / span 2; + font-size: 13px; + font-weight: 950; + height: 30px; + justify-content: center; + width: 30px; } -.outcome-steps strong { - font-size: 14px; - line-height: 1.2; +.console-header p { + line-height: 1.15; + margin: 0; } -.outcome-steps small { - color: rgba(255, 255, 255, 0.52); - font-size: 12px; - line-height: 1.4; +.console-header strong, +.console-header small { + display: block; } -.outcome-note { - border-top: 1px solid rgba(255, 255, 255, 0.08); - color: rgba(255, 255, 255, 0.52); +.console-header strong { + font-family: "SFMono-Regular", Consolas, monospace; font-size: 10px; - font-weight: 800; - letter-spacing: 0.05em; - margin: 24px -34px -34px; - padding: 15px 34px; - text-transform: uppercase; + letter-spacing: 0.1em; } -.hero-proof { - transform: rotate(1.2deg); +.console-header small { + color: rgba(255, 255, 255, 0.36); + font-size: 8px; + letter-spacing: 0.12em; + margin-top: 4px; } -.window-bar { +.console-status { align-items: center; - background: rgba(255, 255, 255, 0.035); - border-bottom: 1px solid rgba(255, 255, 255, 0.08); - display: flex; + border: 1px solid rgba(105, 230, 184, 0.17); + border-radius: 99px; + color: #a5d8c5; + display: inline-flex; + font-family: "SFMono-Regular", Consolas, monospace; + font-size: 8px; + font-weight: 800; gap: 7px; - min-height: 47px; - padding: 0 16px; + letter-spacing: 0.08em; + padding: 7px 9px; } -.window-bar > span { - background: rgba(255, 255, 255, 0.16); +.console-status i { + animation: consolePulse 2.2s ease-in-out infinite; + background: var(--mint); border-radius: 50%; - height: 8px; - width: 8px; + box-shadow: 0 0 12px var(--mint); + height: 5px; + width: 5px; } -.window-bar > span:first-child { background: var(--coral); } -.window-bar > span:nth-child(2) { background: var(--gold); } -.window-bar > span:nth-child(3) { background: var(--mint); } - -.window-bar code { - color: rgba(255, 255, 255, 0.42); - font-family: "SFMono-Regular", Consolas, monospace; - font-size: 11px; - margin-left: 8px; +.contract-map { + height: 430px; overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; + position: relative; } -.hero-proof pre, -.terminal pre { - margin: 0; - overflow: auto; - padding: 28px; +.map-grid { + background: + radial-gradient(circle at center, rgba(105, 230, 184, 0.1), transparent 33%), + linear-gradient(90deg, transparent 49.8%, rgba(105, 230, 184, 0.075) 50%, transparent 50.2%), + linear-gradient(transparent 49.8%, rgba(105, 230, 184, 0.075) 50%, transparent 50.2%); + inset: 0; + position: absolute; } -.hero-proof pre code, -.terminal pre code { - font-family: "SFMono-Regular", Consolas, monospace; - font-size: clamp(11px, 1vw, 14px); - line-height: 1.75; +.map-grid::before, +.map-grid::after { + border: 1px solid rgba(105, 230, 184, 0.1); + border-radius: 50%; + content: ""; + height: 290px; + left: 50%; + position: absolute; + top: 50%; + transform: translate(-50%, -50%); + width: 290px; } -.code-muted { color: #8ba097; } -.code-key { color: #a992f5; } -.code-value { color: #8ee9c6; } -.code-bool { color: #ff9f94; } -.prompt { color: var(--mint); } +.map-grid::after { + border-color: rgba(169, 146, 245, 0.1); + height: 210px; + width: 210px; +} -.hero-proof > p { - border-top: 1px solid rgba(255, 255, 255, 0.07); - color: rgba(255, 255, 255, 0.52); - font-size: 11px; - font-weight: 700; - letter-spacing: 0.04em; - margin: 0; - padding: 13px 18px; - text-transform: uppercase; +.contract-core { + align-items: center; + display: flex; + height: 190px; + justify-content: center; + left: 50%; + position: absolute; + top: 50%; + transform: translate(-50%, -50%); + width: 190px; } -.status-dot { - background: var(--mint); +.core-ring { + border: 1px solid rgba(105, 230, 184, 0.4); border-radius: 50%; - box-shadow: 0 0 12px var(--mint); - display: inline-block; - height: 7px; - margin-right: 7px; - width: 7px; + inset: 0; + position: absolute; } -.question-band { - background: #e8f7f0; - border-bottom: 1px solid #cde5da; - padding: 68px max(24px, calc((100vw - 1180px) / 2)); - text-align: center; +.core-ring::before, +.core-ring::after { + background: var(--mint); + border-radius: 50%; + box-shadow: 0 0 16px var(--mint); + content: ""; + height: 6px; + left: 50%; + position: absolute; + top: -3px; + width: 6px; } -.question-band blockquote { - font-size: clamp(28px, 3.5vw, 48px); - font-weight: 800; - letter-spacing: -0.045em; - line-height: 1.12; - margin: 0 auto 18px; - max-width: 980px; +.core-ring::after { + bottom: -3px; + left: auto; + right: 50%; + top: auto; } -.question-band .answer { - color: var(--muted); - margin-bottom: 0; -} +.ring-a { animation: orbitSpin 16s linear infinite; } +.ring-b { animation: orbitSpinReverse 11s linear infinite; border-color: rgba(169, 146, 245, 0.28); inset: 18px; } +.ring-b::before, .ring-b::after { background: var(--violet); box-shadow: 0 0 16px var(--violet); } -.benefits-grid { - display: grid; - gap: 16px; - grid-template-columns: repeat(3, 1fr); +.contract-core > div { + align-items: center; + background: radial-gradient(circle at 40% 25%, #254f3f, #0d1e17 66%); + border: 1px solid rgba(105, 230, 184, 0.34); + border-radius: 50%; + box-shadow: 0 0 50px rgba(105, 230, 184, 0.13), 0 0 0 8px rgba(105, 230, 184, 0.03); + display: flex; + flex-direction: column; + height: 128px; + justify-content: center; + position: relative; + width: 128px; } -.benefits-grid article { - background: white; - border: 1px solid var(--line); - border-radius: var(--radius); - min-height: 230px; - padding: 26px; -} +.contract-core small { color: rgba(255, 255, 255, 0.42); font-family: "SFMono-Regular", Consolas, monospace; font-size: 7px; letter-spacing: 0.18em; } +.contract-core strong { color: #d9fff0; font-family: "SFMono-Regular", Consolas, monospace; font-size: 12px; letter-spacing: 0.1em; margin-top: 3px; } +.contract-core b { color: var(--mint); font-family: "SFMono-Regular", Consolas, monospace; font-size: 8px; margin-top: 7px; } -.benefits-grid article > span { +.map-node { align-items: center; - background: #e8f7f0; - border-radius: 9px; - color: var(--mint-deep); + background: #10231b; + border: 1px solid rgba(105, 230, 184, 0.18); + border-radius: 10px; + box-shadow: 0 14px 34px rgba(0, 0, 0, 0.25); display: flex; - font-size: 20px; - font-weight: 900; - height: 40px; - justify-content: center; - margin-bottom: 38px; - width: 40px; + gap: 10px; + min-width: 145px; + padding: 10px 12px; + position: absolute; } -.benefits-grid p { - color: var(--muted); - font-size: 14px; +.map-node::after { background: linear-gradient(90deg, rgba(105, 230, 184, 0.35), transparent); content: ""; height: 1px; position: absolute; width: 58px; } +.map-node > span { align-items: center; background: rgba(105, 230, 184, 0.08); border-radius: 6px; color: var(--mint); display: flex; font-family: "SFMono-Regular", Consolas, monospace; font-size: 8px; font-weight: 900; height: 26px; justify-content: center; width: 26px; } +.map-node p { line-height: 1.1; margin: 0; } +.map-node small, .map-node strong { display: block; } +.map-node small { color: rgba(255, 255, 255, 0.34); font-family: "SFMono-Regular", Consolas, monospace; font-size: 6px; letter-spacing: 0.14em; margin-bottom: 4px; } +.map-node strong { color: rgba(255, 255, 255, 0.82); font-size: 10px; } +.node-need { left: 24px; top: 54px; } +.node-rules { right: 24px; top: 54px; } +.node-build { bottom: 50px; left: 24px; } +.node-proof { bottom: 50px; right: 24px; } +.node-need::after, .node-build::after { left: 100%; top: 50%; } +.node-rules::after, .node-proof::after { background: linear-gradient(90deg, transparent, rgba(105, 230, 184, 0.35)); right: 100%; top: 50%; } + +.console-footer { + align-items: center; + border-top: 1px solid rgba(255, 255, 255, 0.075); + display: grid; + gap: 18px; + grid-template-columns: repeat(3, auto) 1fr; + min-height: 72px; + padding: 0 24px; +} + +.console-footer small, .console-footer strong { display: block; font-family: "SFMono-Regular", Consolas, monospace; } +.console-footer small { color: rgba(255, 255, 255, 0.3); font-size: 6px; letter-spacing: 0.12em; } +.console-footer strong { color: #b3d8ca; font-size: 8px; letter-spacing: 0.08em; margin-top: 4px; } +.console-wave { align-items: center; display: flex; gap: 3px; justify-content: flex-end; } +.console-wave i { animation: wave 1.8s ease-in-out infinite; background: var(--mint); border-radius: 2px; height: 8px; opacity: 0.5; width: 2px; } +.console-wave i:nth-child(2) { animation-delay: -0.2s; } +.console-wave i:nth-child(3) { animation-delay: -0.4s; } +.console-wave i:nth-child(4) { animation-delay: -0.6s; } +.console-wave i:nth-child(5) { animation-delay: -0.8s; } +.console-wave i:nth-child(6) { animation-delay: -1s; } +.console-wave i:nth-child(7) { animation-delay: -1.2s; } +.console-wave i:nth-child(8) { animation-delay: -1.4s; } + +.section { + margin: 0 auto; + max-width: 1260px; + padding: 120px 32px; + position: relative; +} + +.section-heading { + align-items: end; + display: grid; + gap: 70px; + grid-template-columns: 1.15fr 0.65fr; + margin-bottom: 58px; +} + +.section-heading.compact { + display: block; + max-width: 900px; +} + +.section-heading h2, +.section-heading p { margin-bottom: 0; } -.collaboration-band { - background: var(--dark); - color: white; +.section-heading > p { + color: var(--muted); + font-size: 16px; } -.collaboration-layout { - display: grid; - gap: 48px; - grid-template-columns: 0.72fr 1.28fr; +.question-band { + background: + linear-gradient(90deg, rgba(105, 230, 184, 0.03), rgba(169, 146, 245, 0.05), rgba(105, 230, 184, 0.03)), + #07100d; + border-bottom: 1px solid var(--line); + border-top: 1px solid var(--line); + overflow: hidden; + padding: 96px max(24px, calc((100vw - 1160px) / 2)); + position: relative; + text-align: center; } -.collaboration-intro > p:last-child { - color: rgba(255, 255, 255, 0.62); - font-size: 18px; +.question-band::before { + color: rgba(105, 230, 184, 0.035); + content: "?"; + font-size: 340px; + font-weight: 950; + left: 50%; + line-height: 1; + position: absolute; + top: 50%; + transform: translate(-50%, -52%); } -.partner-grid { +.question-band > * { + position: relative; +} + +.question-band blockquote { + font-size: clamp(30px, 3.6vw, 52px); + font-weight: 780; + letter-spacing: -0.05em; + line-height: 1.08; + margin: 0 auto 20px; + max-width: 1020px; +} + +.question-band .answer { + color: var(--muted); + margin-bottom: 0; +} + +.benefits-section::before { + background: radial-gradient(circle, rgba(105, 230, 184, 0.08), transparent 66%); + content: ""; + height: 520px; + left: -300px; + pointer-events: none; + position: absolute; + top: 60px; + width: 520px; +} + +.benefits-grid { display: grid; - gap: 16px; - grid-template-columns: 1fr 1fr; + gap: 14px; + grid-template-columns: repeat(12, 1fr); } -.partner-grid article { - background: rgba(255, 255, 255, 0.055); - border: 1px solid rgba(255, 255, 255, 0.1); +.benefits-grid article { + background: + linear-gradient(145deg, rgba(255, 255, 255, 0.035), transparent 70%), + var(--panel); + border: 1px solid var(--line); border-radius: var(--radius); + grid-column: span 4; + min-height: 250px; + overflow: hidden; padding: 28px; + position: relative; + transition: border-color 180ms ease, transform 180ms ease; } -.partner-label { - font-size: 10px; - font-weight: 900; - letter-spacing: 0.1em; - margin-bottom: 13px; - text-transform: uppercase; +.benefits-grid article::after { + background: radial-gradient(circle, rgba(105, 230, 184, 0.13), transparent 66%); + content: ""; + height: 180px; + position: absolute; + right: -90px; + top: -90px; + width: 180px; } -.partner-label.human { +.benefits-grid article:nth-child(2)::after, +.benefits-grid article:nth-child(5)::after { + background: radial-gradient(circle, rgba(169, 146, 245, 0.14), transparent 66%); +} + +.benefits-grid article:hover { + border-color: var(--line-bright); + transform: translateY(-4px); +} + +.benefits-grid article > span { + align-items: center; + background: rgba(105, 230, 184, 0.08); + border: 1px solid rgba(105, 230, 184, 0.14); + border-radius: 9px; color: var(--mint); + display: flex; + font-family: "SFMono-Regular", Consolas, monospace; + font-size: 18px; + font-weight: 900; + height: 42px; + justify-content: center; + margin-bottom: 48px; + width: 42px; } -.partner-label.ai { - color: #c5b9ff; +.benefits-grid h3 { + margin-bottom: 12px; } -.partner-grid ul { - color: rgba(255, 255, 255, 0.64); - font-size: 13px; - line-height: 1.8; - margin: 20px 0 0; - padding-left: 18px; +.benefits-grid p { + color: var(--muted); + font-size: 14px; + margin-bottom: 0; } +.collaboration-band { + background: + radial-gradient(circle at 72% 42%, rgba(169, 146, 245, 0.13), transparent 30%), + radial-gradient(circle at 18% 70%, rgba(105, 230, 184, 0.09), transparent 28%), + #08110e; + border-bottom: 1px solid var(--line); + border-top: 1px solid var(--line); + position: relative; +} + +.collaboration-layout { + display: grid; + gap: 54px; + grid-template-columns: 0.72fr 1.28fr; +} + +.collaboration-intro > p:last-child { + color: var(--muted); + font-size: 17px; +} + +.partner-grid { + display: grid; + gap: 14px; + grid-template-columns: 1fr 1fr; + position: relative; +} + +.partner-grid::before { + background: linear-gradient(180deg, var(--mint), var(--violet)); + content: ""; + left: 50%; + opacity: 0.4; + position: absolute; + top: 36px; + transform: translateX(-50%); + width: 1px; + height: calc(100% - 72px); +} + +.partner-grid article { + background: rgba(15, 31, 25, 0.88); + border: 1px solid var(--line); + border-radius: var(--radius); + min-height: 330px; + padding: 30px; + position: relative; +} + +.partner-grid article:first-child { box-shadow: inset 0 3px 0 rgba(105, 230, 184, 0.55); } +.partner-grid article:last-child { box-shadow: inset 0 3px 0 rgba(169, 146, 245, 0.55); } +.partner-label { font-size: 9px; font-weight: 900; letter-spacing: 0.14em; margin-bottom: 18px; text-transform: uppercase; } +.partner-label.human { color: var(--mint); } +.partner-label.ai { color: var(--violet); } +.partner-grid ul { color: var(--muted); font-size: 13px; line-height: 1.9; margin: 22px 0 0; padding-left: 18px; } + .alignment-strip { align-items: center; - background: rgba(105, 230, 184, 0.08); - border: 1px solid rgba(105, 230, 184, 0.18); + background: rgba(105, 230, 184, 0.04); + border: 1px solid rgba(105, 230, 184, 0.14); border-radius: 12px; display: flex; gap: 14px; grid-column: 1 / -1; justify-content: center; - padding: 17px; + padding: 18px; } -.alignment-strip span { - color: #cce4db; - font-size: 11px; - font-weight: 800; - text-transform: uppercase; +.alignment-strip span { color: #afc8be; font-size: 9px; font-weight: 900; letter-spacing: 0.07em; text-transform: uppercase; } +.alignment-strip b { color: var(--mint); } + +.demo-band { + background: + linear-gradient(90deg, rgba(5, 11, 9, 0.96) 0%, rgba(5, 11, 9, 0.76) 43%, transparent 70%), + radial-gradient(circle at 78% 50%, rgba(45, 171, 124, 0.14), transparent 36%), + #050b09; + overflow: hidden; + position: relative; } -.alignment-strip b { - color: var(--mint); +.demo-band::after { + background: linear-gradient(90deg, transparent, rgba(105, 230, 184, 0.18), transparent); + bottom: 0; + content: ""; + height: 1px; + left: 0; + position: absolute; + right: 0; } -.technical-intro { - background: linear-gradient(135deg, #1b3c31, #0b1713); - color: white; - padding: 80px max(24px, calc((100vw - 1180px) / 2)); - text-align: center; +.demo-layout { + align-items: center; + display: grid; + gap: clamp(50px, 7vw, 90px); + grid-template-columns: 0.72fr 1.28fr; + max-width: 1380px; } -.technical-intro h2 { - margin-left: auto; - margin-right: auto; - max-width: 900px; +.demo-copy > p:not(.section-label) { + color: var(--muted); + font-size: 17px; } -.technical-intro > p:last-child { - color: rgba(255, 255, 255, 0.62); - font-size: 18px; - margin: 0 auto; - max-width: 700px; +.demo-stack { + display: flex; + flex-wrap: wrap; + gap: 7px; + list-style: none; + margin: 28px 0 34px; + padding: 0; } -.section { - margin: 0 auto; - max-width: 1240px; - padding: 112px 32px; +.demo-stack li { + background: rgba(105, 230, 184, 0.045); + border: 1px solid rgba(105, 230, 184, 0.16); + border-radius: 99px; + color: #a7c9bc; + font-size: 9px; + font-weight: 850; + letter-spacing: 0.03em; + padding: 7px 10px; } -.section-heading { - align-items: end; - display: grid; - gap: 60px; - grid-template-columns: 1.25fr 0.75fr; - margin-bottom: 54px; +.product-window { + background: #09110e; + border: 1px solid rgba(105, 230, 184, 0.2); + border-radius: 22px; + box-shadow: 0 44px 120px rgba(0, 0, 0, 0.6), 0 0 70px rgba(105, 230, 184, 0.07); + color: var(--ink); + min-width: 680px; + overflow: hidden; + transform: perspective(1400px) rotateY(-4deg) rotateX(1deg); } -.section-heading.compact { - display: block; - max-width: 920px; +.product-topbar { + align-items: center; + background: #0d1a15; + border-bottom: 1px solid var(--line); + display: flex; + height: 58px; + justify-content: space-between; + padding: 0 20px; } -.section-heading h2, -.section-heading p { - margin-bottom: 0; +.product-brand { font-size: 13px; font-weight: 900; } +.product-brand span { color: var(--mint); margin-right: 6px; } +.product-user { background: linear-gradient(145deg, var(--mint), var(--violet)); border-radius: 50%; box-shadow: 0 0 16px rgba(105, 230, 184, 0.2); height: 27px; width: 27px; } +.product-body { display: grid; grid-template-columns: 60px 1fr; min-height: 410px; } +.product-body aside { background: #0b1712; border-right: 1px solid var(--line); display: flex; flex-direction: column; gap: 24px; padding: 27px 20px; } +.product-body aside span { background: #263b32; border-radius: 5px; height: 14px; width: 18px; } +.product-body aside span.active { background: var(--mint); box-shadow: 0 0 12px rgba(105, 230, 184, 0.3); } +.board { background: linear-gradient(145deg, #101e18, #0b1511); padding: 28px; } +.board-header { align-items: end; display: flex; justify-content: space-between; margin-bottom: 26px; } +.board-header small { color: var(--dim); display: block; font-size: 7px; font-weight: 850; letter-spacing: 0.12em; } +.board-header strong { display: block; font-size: 23px; letter-spacing: -0.04em; } +.board-header button { background: var(--mint); border: 0; border-radius: 7px; color: var(--void); font-size: 9px; font-weight: 900; padding: 9px 12px; } +.columns { display: grid; gap: 11px; grid-template-columns: repeat(3, 1fr); } +.column { background: rgba(255, 255, 255, 0.025); border: 1px solid rgba(255, 255, 255, 0.045); border-radius: 10px; min-height: 285px; padding: 12px; } +.column > p { align-items: center; color: #c0d0ca; display: flex; font-size: 8px; font-weight: 850; letter-spacing: 0.03em; margin-bottom: 12px; text-transform: uppercase; } +.column > p b { color: var(--dim); margin-left: auto; } +.dot { border-radius: 50%; display: inline-block; height: 6px; margin-right: 6px; width: 6px; } +.dot.coral { background: var(--coral); } +.dot.gold { background: var(--gold); } +.dot.green { background: var(--mint); } +.task { background: #14241d; border: 1px solid rgba(255, 255, 255, 0.055); border-radius: 8px; margin-bottom: 9px; padding: 13px; } +.task .tag { border-radius: 99px; display: block; height: 3px; margin-bottom: 10px; width: 28px; } +.tag.green { background: var(--mint); } +.tag.violet { background: var(--violet); } +.tag.blue { background: var(--blue); } +.task strong { display: block; font-size: 10px; line-height: 1.3; } +.task small { color: var(--dim); display: block; font-size: 7px; margin-top: 10px; } +.task.done strong { color: #697c73; text-decoration: line-through; } + +.technical-intro { + background: + linear-gradient(90deg, transparent, rgba(105, 230, 184, 0.05), transparent), + #07100d; + border-bottom: 1px solid var(--line); + border-top: 1px solid var(--line); + padding: 100px max(24px, calc((100vw - 1180px) / 2)); + position: relative; + text-align: center; } -.section-heading > p { - color: var(--muted); - font-size: 18px; +.technical-intro::before { + background: linear-gradient(90deg, transparent, var(--mint), var(--violet), transparent); + content: ""; + height: 1px; + left: 20%; + opacity: 0.55; + position: absolute; + right: 20%; + top: 0; +} + +.technical-intro h2 { margin-left: auto; margin-right: auto; max-width: 900px; } +.technical-intro > p:last-child { color: var(--muted); font-size: 17px; margin: 0 auto; max-width: 700px; } + +.model-section { + counter-reset: model; } .model-grid { display: grid; - gap: 1px; + gap: 12px; grid-template-columns: repeat(4, 1fr); list-style: none; margin: 0; padding: 0; + position: relative; +} + +.model-grid::before { + background: linear-gradient(90deg, var(--mint), var(--violet)); + content: ""; + height: 1px; + left: 8%; + opacity: 0.32; + position: absolute; + right: 8%; + top: 51px; } .model-grid li { - background: white; + background: var(--panel); border: 1px solid var(--line); + border-radius: var(--radius); display: flex; flex-direction: column; - min-height: 300px; - padding: 26px; + min-height: 285px; + padding: 24px; + position: relative; } -.model-grid li:first-child { border-radius: var(--radius) 0 0 var(--radius); } -.model-grid li:last-child { border-radius: 0 var(--radius) var(--radius) 0; } - .step-number { - color: var(--coral); + align-items: center; + background: #0a1712; + border: 1px solid var(--line-bright); + border-radius: 50%; + color: var(--mint); + display: flex; font-family: "SFMono-Regular", Consolas, monospace; - font-size: 12px; + font-size: 9px; font-weight: 900; + height: 54px; + justify-content: center; margin-bottom: auto; + position: relative; + width: 54px; + z-index: 2; } -.model-grid p { - color: var(--muted); - font-size: 15px; - margin-bottom: 0; -} +.model-grid h3 { font-size: 20px; } +.model-grid p { color: var(--muted); font-size: 13px; margin-bottom: 0; } .anatomy-band { - background: #fff6ec; - border-bottom: 1px solid #eadfce; - border-top: 1px solid #eadfce; + background: + radial-gradient(circle at 76% 48%, rgba(105, 230, 184, 0.11), transparent 30%), + #07100d; + border-bottom: 1px solid var(--line); + border-top: 1px solid var(--line); } .anatomy-layout { align-items: center; display: grid; gap: clamp(50px, 8vw, 120px); - grid-template-columns: 0.85fr 1.15fr; -} - -.anatomy-copy > p:not(.section-label) { - color: var(--muted); - font-size: 18px; - margin-bottom: 26px; + grid-template-columns: 0.8fr 1.2fr; } -.text-link { - border-bottom: 1px solid currentColor; - color: var(--mint-deep); - font-size: 14px; - font-weight: 850; - padding-bottom: 3px; -} +.anatomy-copy > p:not(.section-label) { color: var(--muted); font-size: 17px; margin-bottom: 26px; } +.text-link { border-bottom: 1px solid currentColor; color: var(--mint); font-size: 12px; font-weight: 850; padding-bottom: 3px; } .file-tree { - background: var(--dark); + background: #091510; + border: 1px solid var(--line-bright); border-radius: var(--radius); - box-shadow: var(--shadow); - color: #eaf5ef; - padding: 26px; + box-shadow: var(--shadow), 0 0 70px rgba(105, 230, 184, 0.06); + padding: 24px; + position: relative; +} + +.file-tree::before { + background: linear-gradient(90deg, var(--mint), var(--violet)); + content: ""; + height: 2px; + left: 24px; + position: absolute; + right: 24px; + top: 0; } .file-tree div { align-items: center; - border-bottom: 1px solid rgba(255, 255, 255, 0.07); + border-bottom: 1px solid rgba(255, 255, 255, 0.06); display: grid; font-family: "SFMono-Regular", Consolas, monospace; - font-size: 13px; + font-size: 12px; gap: 10px; - grid-template-columns: 18px minmax(180px, 1fr) 1fr; - min-height: 50px; + grid-template-columns: 18px minmax(190px, 1fr) 1fr; + min-height: 52px; padding-left: 22px; } .file-tree div:last-child { border-bottom: 0; } .file-tree div span { color: var(--mint); } -.file-tree div small { color: #84978f; font-family: Inter, sans-serif; } -.file-tree .tree-root { font-size: 15px; font-weight: 800; grid-template-columns: 18px 1fr; padding-left: 0; } +.file-tree div small { color: var(--dim); font-family: Inter, sans-serif; } +.file-tree .tree-root { color: var(--mint-hot); font-size: 14px; font-weight: 850; grid-template-columns: 18px 1fr; padding-left: 0; } + +.architecture-section::before { + background: radial-gradient(circle, rgba(169, 146, 245, 0.08), transparent 68%); + content: ""; + height: 600px; + pointer-events: none; + position: absolute; + right: -350px; + top: 100px; + width: 600px; +} .architecture-grid { display: grid; - gap: 24px; + gap: 14px; grid-template-columns: 1fr 1fr; } .architecture-card { + background: var(--panel); border: 1px solid var(--line); border-radius: var(--radius); overflow: hidden; - padding: 34px; + padding: 32px; position: relative; } .architecture-card::before { content: ""; - height: 4px; + height: 3px; left: 0; position: absolute; right: 0; top: 0; } -.backend-card { background: #eaf7f2; } -.frontend-card { background: #f1effb; } -.backend-card::before { background: var(--mint-deep); } +.backend-card { background: linear-gradient(145deg, rgba(105, 230, 184, 0.07), transparent 55%), var(--panel); } +.frontend-card { background: linear-gradient(145deg, rgba(169, 146, 245, 0.08), transparent 55%), var(--panel); } +.backend-card::before { background: var(--mint); } .frontend-card::before { background: var(--violet); } - -.card-heading { - align-items: center; - display: flex; - gap: 14px; - margin-bottom: 24px; -} - -.card-icon { - align-items: center; - background: var(--dark); - border-radius: 10px; - color: white; - display: flex; - font-family: "SFMono-Regular", Consolas, monospace; - font-weight: 900; - height: 44px; - justify-content: center; - width: 44px; -} - -.frontend-card .card-icon { background: #4f407f; } -.card-heading p { color: var(--muted); font-size: 11px; font-weight: 800; letter-spacing: 0.08em; margin: 0; text-transform: uppercase; } -.card-heading h3 { font-size: 28px; margin: 0; } -.card-lede { color: #4e6258; min-height: 82px; } +.card-heading { align-items: center; display: flex; gap: 14px; margin-bottom: 24px; } +.card-icon { align-items: center; background: rgba(105, 230, 184, 0.1); border: 1px solid rgba(105, 230, 184, 0.2); border-radius: 10px; color: var(--mint); display: flex; font-family: "SFMono-Regular", Consolas, monospace; font-weight: 900; height: 44px; justify-content: center; width: 44px; } +.frontend-card .card-icon { background: rgba(169, 146, 245, 0.1); border-color: rgba(169, 146, 245, 0.2); color: var(--violet); } +.card-heading p { color: var(--dim); font-size: 8px; font-weight: 850; letter-spacing: 0.1em; margin: 0; text-transform: uppercase; } +.card-heading h3 { font-size: 27px; margin: 0; } +.card-lede { color: var(--muted); min-height: 80px; } .mini-flow { - background: rgba(255, 255, 255, 0.72); - border: 1px solid rgba(16, 32, 25, 0.08); + background: rgba(0, 0, 0, 0.18); + border: 1px solid rgba(255, 255, 255, 0.055); border-radius: 12px; list-style: none; margin: 25px 0; padding: 8px 20px; } -.mini-flow li { - border-bottom: 1px solid rgba(16, 32, 25, 0.08); - font-size: 14px; - font-weight: 750; - padding: 13px 0; -} - +.mini-flow li { border-bottom: 1px solid rgba(255, 255, 255, 0.055); color: #c4d5ce; font-size: 12px; font-weight: 750; padding: 13px 0; } .mini-flow li:last-child { border-bottom: 0; } -.mini-flow span { color: var(--mint-deep); font-family: "SFMono-Regular", Consolas, monospace; font-size: 10px; margin-right: 12px; } -.frontend-card .mini-flow span { color: #6955a9; } -.boundary-note { color: var(--muted); font-size: 12px; font-weight: 700; margin-bottom: 0; } +.mini-flow span { color: var(--mint); font-family: "SFMono-Regular", Consolas, monospace; font-size: 8px; margin-right: 12px; } +.frontend-card .mini-flow span { color: var(--violet); } +.boundary-note { color: var(--dim); font-size: 10px; font-weight: 750; margin-bottom: 0; } .shared-runtime { display: grid; - gap: 12px; + gap: 10px; grid-template-columns: repeat(4, 1fr); - margin-top: 24px; + margin-top: 14px; } .shared-runtime span { - background: var(--dark); + background: #0a1712; + border: 1px solid var(--line); border-radius: 10px; - color: rgba(255, 255, 255, 0.72); - font-size: 12px; + color: #9db8ad; + font-family: "SFMono-Regular", Consolas, monospace; + font-size: 8px; font-weight: 800; - padding: 14px; + letter-spacing: 0.06em; + padding: 15px; text-align: center; + text-transform: uppercase; } .code-examples-band { - background: #0d1d17; - color: white; -} + background: + linear-gradient(180deg, rgba(105, 230, 184, 0.025), transparent 40%), + #060e0b; + border-bottom: 1px solid var(--line); + border-top: 1px solid var(--line); +} + +.code-examples-band .section-heading > p { color: var(--muted); } +.code-examples { display: grid; gap: 14px; grid-template-columns: repeat(3, 1fr); } +.code-examples article { background: #0b1813; border: 1px solid var(--line); border-radius: var(--radius); overflow: hidden; transition: border-color 160ms ease, transform 160ms ease; } +.code-examples article:hover { border-color: var(--line-bright); transform: translateY(-3px); } +.code-examples header { align-items: center; display: flex; gap: 13px; padding: 20px; } +.code-examples header > span { align-items: center; background: rgba(105, 230, 184, 0.08); border: 1px solid rgba(105, 230, 184, 0.14); border-radius: 8px; color: var(--mint); display: flex; font-family: "SFMono-Regular", Consolas, monospace; font-size: 8px; font-weight: 900; height: 34px; justify-content: center; width: 34px; } +.code-examples header small { color: var(--dim); display: block; font-size: 7px; font-weight: 850; letter-spacing: 0.12em; text-transform: uppercase; } +.code-examples header h3 { font-size: 17px; margin: 2px 0 0; } +.code-examples pre { background: #050b09; border-bottom: 1px solid rgba(255, 255, 255, 0.05); border-top: 1px solid rgba(255, 255, 255, 0.05); margin: 0; min-height: 250px; overflow: auto; padding: 21px 17px; } +.code-examples pre code { color: #d4e7de; font-family: "SFMono-Regular", Consolas, monospace; font-size: 10px; line-height: 1.85; } +.code-examples article > p { color: var(--dim); font-size: 11px; margin: 0; padding: 17px 20px 21px; } +.code-muted { color: #738a80; } +.code-key { color: var(--violet); } +.code-value { color: var(--mint); } +.code-bool { color: var(--coral); } +.prompt { color: var(--mint); } -.code-examples-band .section-heading > p { - color: rgba(255, 255, 255, 0.58); +.docs-section { + padding-bottom: 130px; } -.code-examples { +.docs-grid { display: grid; - gap: 18px; - grid-template-columns: repeat(3, 1fr); + gap: 14px; + grid-template-columns: repeat(4, 1fr); } -.code-examples article { - background: #13271f; - border: 1px solid rgba(105, 230, 184, 0.14); +.docs-grid a { + background: linear-gradient(150deg, rgba(255, 255, 255, 0.035), transparent 66%), var(--panel); + border: 1px solid var(--line); border-radius: var(--radius); + min-height: 300px; overflow: hidden; + padding: 25px; + position: relative; + transition: border-color 160ms ease, box-shadow 160ms ease, transform 160ms ease; } -.code-examples header { - align-items: center; - display: flex; - gap: 13px; - padding: 22px; -} - -.code-examples header > span { - align-items: center; - background: rgba(105, 230, 184, 0.1); - border-radius: 8px; - color: var(--mint); - display: flex; - font-family: "SFMono-Regular", Consolas, monospace; - font-size: 10px; - font-weight: 900; - height: 36px; - justify-content: center; - width: 36px; -} - -.code-examples header small { - color: rgba(255, 255, 255, 0.42); - display: block; - font-size: 9px; - font-weight: 800; - letter-spacing: 0.1em; - text-transform: uppercase; -} - -.code-examples header h3 { - font-size: 18px; - margin: 2px 0 0; -} - -.code-examples pre { - background: #09140f; - border-bottom: 1px solid rgba(255, 255, 255, 0.06); - border-top: 1px solid rgba(255, 255, 255, 0.06); - margin: 0; - min-height: 250px; - overflow: auto; - padding: 22px 18px; -} - -.code-examples pre code { - color: #dcebe5; - font-family: "SFMono-Regular", Consolas, monospace; - font-size: 11px; - line-height: 1.8; +.docs-grid a::after { + background: linear-gradient(90deg, var(--mint), var(--violet)); + bottom: 0; + content: ""; + height: 2px; + left: 24px; + opacity: 0; + position: absolute; + right: 24px; + transition: opacity 160ms ease; } -.code-examples article > p { - color: rgba(255, 255, 255, 0.52); - font-size: 12px; - margin: 0; - padding: 18px 22px 22px; +.docs-grid a:hover, +.docs-grid a:focus-visible { + border-color: var(--line-bright); + box-shadow: 0 22px 54px rgba(0, 0, 0, 0.26); + transform: translateY(-5px); } -.demo-band { - background: #142a22; - color: white; - overflow: hidden; -} +.docs-grid a:hover::after, +.docs-grid a:focus-visible::after { opacity: 1; } +.docs-grid span { color: var(--coral); font-family: "SFMono-Regular", Consolas, monospace; font-size: 9px; font-weight: 900; } +.docs-grid h3 { margin-top: 54px; } +.docs-grid p { color: var(--muted); font-size: 13px; } +.docs-grid b { color: var(--mint); display: block; font-size: 10px; margin-top: 25px; } -.demo-layout { +.start-band { align-items: center; + background: + radial-gradient(circle at 78% 50%, rgba(105, 230, 184, 0.11), transparent 27%), + linear-gradient(135deg, #0c1d17, #050b09); + border-bottom: 1px solid var(--line); + border-top: 1px solid var(--line); display: grid; - gap: clamp(50px, 7vw, 90px); - grid-template-columns: 0.74fr 1.26fr; - max-width: 1380px; -} - -.demo-copy > p:not(.section-label) { - color: rgba(255, 255, 255, 0.66); - font-size: 18px; -} - -.demo-stack { - display: flex; - flex-wrap: wrap; - gap: 8px; - list-style: none; - margin: 28px 0 34px; - padding: 0; + gap: 72px; + grid-template-columns: 0.78fr 1.22fr; + padding: 100px max(32px, calc((100vw - 1180px) / 2)); } -.demo-stack li { - border: 1px solid rgba(105, 230, 184, 0.25); - border-radius: 99px; - color: #b7ddcf; - font-size: 11px; - font-weight: 800; - padding: 7px 11px; -} +.start-band > div:first-child > p:last-child { color: var(--muted); font-size: 16px; } -.product-window { - background: #f5f7f4; - border: 1px solid rgba(255, 255, 255, 0.12); - border-radius: 20px; - box-shadow: 0 45px 100px rgba(0, 0, 0, 0.4); - color: var(--ink); - min-width: 680px; +.terminal { + background: #060d0a; + border: 1px solid rgba(105, 230, 184, 0.2); + border-radius: var(--radius); + box-shadow: var(--shadow), 0 0 60px rgba(105, 230, 184, 0.05); overflow: hidden; - transform: perspective(1200px) rotateY(-4deg) rotateX(1deg); } -.product-topbar { +.window-bar { align-items: center; - background: white; + background: #0b1712; border-bottom: 1px solid var(--line); display: flex; - height: 58px; - justify-content: space-between; - padding: 0 20px; -} - -.product-brand { font-size: 14px; font-weight: 900; } -.product-brand span { color: var(--mint-deep); margin-right: 6px; } -.product-user { background: #d9eee6; border-radius: 50%; height: 28px; width: 28px; } -.product-body { display: grid; grid-template-columns: 60px 1fr; min-height: 410px; } -.product-body aside { background: white; border-right: 1px solid var(--line); display: flex; flex-direction: column; gap: 24px; padding: 27px 20px; } -.product-body aside span { background: #dce5e0; border-radius: 5px; height: 14px; width: 18px; } -.product-body aside span.active { background: var(--mint-deep); } -.board { padding: 28px; } -.board-header { align-items: end; display: flex; justify-content: space-between; margin-bottom: 26px; } -.board-header small { color: #8a9891; display: block; font-size: 8px; font-weight: 850; letter-spacing: 0.1em; } -.board-header strong { display: block; font-size: 24px; letter-spacing: -0.04em; } -.board-header button { background: var(--dark); border: 0; border-radius: 7px; color: white; font-size: 10px; font-weight: 800; padding: 9px 12px; } -.columns { display: grid; gap: 12px; grid-template-columns: repeat(3, 1fr); } -.column { background: #ebefec; border-radius: 10px; min-height: 285px; padding: 12px; } -.column > p { align-items: center; display: flex; font-size: 9px; font-weight: 850; margin-bottom: 12px; text-transform: uppercase; } -.column > p b { color: #8b9691; margin-left: auto; } -.dot { border-radius: 50%; height: 7px; margin-right: 6px; width: 7px; } -.dot.coral { background: var(--coral); } -.dot.gold { background: var(--gold); } -.dot.green { background: var(--mint-deep); } -.task { background: white; border: 1px solid #dee5e1; border-radius: 8px; margin-bottom: 9px; padding: 13px; } -.task .tag { border-radius: 99px; display: block; height: 4px; margin-bottom: 10px; width: 28px; } -.tag.green { background: var(--mint); } -.tag.violet { background: var(--violet); } -.tag.blue { background: var(--blue); } -.task strong { display: block; font-size: 10px; line-height: 1.3; } -.task small { color: #8a9790; display: block; font-size: 8px; margin-top: 10px; } -.task.done strong { color: #77847e; text-decoration: line-through; } - -.docs-grid { - display: grid; - gap: 18px; - grid-template-columns: repeat(4, 1fr); -} - -.docs-grid a { - background: white; - border: 1px solid var(--line); - border-radius: var(--radius); - min-height: 285px; - padding: 25px; - transition: border 160ms ease, box-shadow 160ms ease, transform 160ms ease; -} - -.docs-grid a:hover, -.docs-grid a:focus-visible { - border-color: #9fcdbb; - box-shadow: 0 18px 46px rgba(15, 35, 27, 0.09); - transform: translateY(-4px); -} - -.docs-grid span { color: var(--coral); font-family: "SFMono-Regular", Consolas, monospace; font-size: 11px; font-weight: 900; } -.docs-grid h3 { margin-top: 50px; } -.docs-grid p { color: var(--muted); font-size: 14px; } -.docs-grid b { color: var(--mint-deep); display: block; font-size: 12px; margin-top: 24px; } - -.start-band { - align-items: center; - background: var(--dark); - color: white; - display: grid; - gap: 70px; - grid-template-columns: 0.85fr 1.15fr; - padding: 90px max(32px, calc((100vw - 1180px) / 2)); + gap: 7px; + min-height: 46px; + padding: 0 16px; } -.start-band > div:first-child > p:last-child { color: rgba(255, 255, 255, 0.6); font-size: 18px; } -.start-band .terminal { background: #101f19; box-shadow: none; } +.window-bar > span { background: rgba(255, 255, 255, 0.16); border-radius: 50%; height: 7px; width: 7px; } +.window-bar > span:first-child { background: var(--coral); } +.window-bar > span:nth-child(2) { background: var(--gold); } +.window-bar > span:nth-child(3) { background: var(--mint); } +.window-bar code { color: var(--dim); font-family: "SFMono-Regular", Consolas, monospace; font-size: 9px; margin-left: 8px; } +.terminal pre { margin: 0; overflow: auto; padding: 26px; } +.terminal pre code { color: #cbddd5; font-family: "SFMono-Regular", Consolas, monospace; font-size: clamp(10px, 1vw, 13px); line-height: 1.8; } .footer { align-items: center; - background: #07100d; - color: #8ca097; + background: #030806; + color: var(--dim); display: grid; - font-size: 13px; + font-size: 11px; gap: 30px; grid-template-columns: 1fr auto 1fr; - padding: 36px max(24px, calc((100vw - 1240px) / 2)); + padding: 40px max(24px, calc((100vw - 1240px) / 2)); } .footer > div { align-items: center; display: flex; gap: 12px; } .footer p { margin: 0; } -.footer strong { color: white; } -.footer > a { color: var(--mint); font-weight: 800; justify-self: end; } -.footer p a { color: #bed0c8; text-decoration: underline; } +.footer strong { color: var(--ink); } +.footer > a { color: var(--mint); font-weight: 850; justify-self: end; } +.footer p a { color: #a9beb5; text-decoration: underline; } -@keyframes drift { - 0%, 100% { transform: translateY(0); } - 50% { transform: translateY(-10px); } -} +@keyframes orbitSpin { to { transform: rotate(360deg); } } +@keyframes orbitSpinReverse { to { transform: rotate(-360deg); } } +@keyframes consolePulse { 0%, 100% { opacity: 0.55; transform: scale(0.9); } 50% { opacity: 1; transform: scale(1.25); } } +@keyframes wave { 0%, 100% { height: 6px; opacity: 0.3; } 50% { height: 22px; opacity: 0.9; } } -@media (max-width: 980px) { +@media (max-width: 1040px) { .site-header { grid-template-columns: 1fr auto; } nav { display: none; } - .hero-inner { grid-template-columns: 1fr; padding-bottom: 90px; padding-top: 80px; } + .hero-inner { grid-template-columns: 1fr; padding-bottom: 100px; padding-top: 130px; } .hero-copy { max-width: 780px; } - .hero-proof, .outcome-panel { max-width: 650px; transform: none; } - .system-map { opacity: 0.25; right: -200px; width: 700px; } - .section-heading { align-items: start; gap: 22px; grid-template-columns: 1fr; } - .model-grid { grid-template-columns: 1fr 1fr; } - .model-grid li { min-height: 240px; } - .model-grid li:first-child { border-radius: var(--radius) 0 0 0; } - .model-grid li:nth-child(2) { border-radius: 0 var(--radius) 0 0; } - .model-grid li:nth-child(3) { border-radius: 0 0 0 var(--radius); } - .model-grid li:last-child { border-radius: 0 0 var(--radius) 0; } - .anatomy-layout, .demo-layout, .start-band { grid-template-columns: 1fr; } + .future-console { justify-self: start; max-width: 650px; } + .section-heading { align-items: start; gap: 24px; grid-template-columns: 1fr; } + .benefits-grid article { grid-column: span 6; } .collaboration-layout { grid-template-columns: 1fr; } - .benefits-grid, .code-examples { grid-template-columns: 1fr 1fr; } - .code-examples article:last-child { grid-column: 1 / -1; } - .code-examples pre { min-height: 225px; } + .collaboration-intro { max-width: 760px; } + .demo-layout, .anatomy-layout, .start-band { grid-template-columns: 1fr; } .demo-copy { max-width: 680px; } .product-window { min-width: 0; transform: none; } + .model-grid { grid-template-columns: 1fr 1fr; } + .model-grid::before { display: none; } + .architecture-grid { grid-template-columns: 1fr; } + .card-lede { min-height: 0; } + .code-examples { grid-template-columns: 1fr 1fr; } + .code-examples article:last-child { grid-column: 1 / -1; } + .code-examples pre { min-height: 225px; } .docs-grid { grid-template-columns: 1fr 1fr; } .footer { grid-template-columns: 1fr auto; } .footer > p { display: none; } } @media (max-width: 680px) { - .site-header { height: 64px; padding: 0 18px; } - .header-cta { padding: 9px 12px; } - .hero { min-height: auto; } - .hero-inner { min-height: auto; padding: 70px 20px 72px; } - h1 { font-size: clamp(48px, 15vw, 68px); } - h2 { font-size: 39px; } - .hero-proof pre { padding: 20px 16px; } - .signal-list { gap: 12px 18px; } - .outcome-panel { padding: 24px 18px; } - .outcome-note { margin: 20px -18px -24px; padding: 14px 18px; } - .question-band { padding: 58px 20px; } - .section { padding: 78px 20px; } - .model-grid, .architecture-grid, .docs-grid, .shared-runtime, .benefits-grid, .partner-grid, .code-examples { grid-template-columns: 1fr; } - .benefits-grid article { min-height: 0; } + .site-header { height: 60px; left: 10px; padding: 0 9px 0 12px; right: 10px; top: 9px; } + .brand { font-size: 13px; } + .brand-mark { height: 31px; width: 31px; } + .header-cta { font-size: 10px; padding: 9px 11px; } + h1 { font-size: clamp(46px, 14vw, 64px); } + h2 { font-size: 38px; } + .hero-inner { min-height: auto; padding: 112px 20px 74px; } + .hero-lede { font-size: 18px; } + .signal-list { gap: 12px 17px; } + #aifa-canvas { mask-image: linear-gradient(180deg, transparent 0%, transparent 40%, #000 72%, #000 100%); -webkit-mask-image: linear-gradient(180deg, transparent 0%, transparent 40%, #000 72%, #000 100%); } + .ambient-orbit { display: none; } + .console-header { padding: 0 14px; } + .console-status { font-size: 6px; } + .contract-map { height: 390px; } + .contract-core { height: 154px; width: 154px; } + .contract-core > div { height: 104px; width: 104px; } + .contract-core strong { font-size: 10px; } + .map-grid::before { height: 230px; width: 230px; } + .map-grid::after { height: 170px; width: 170px; } + .map-node { min-width: 124px; padding: 8px; } + .map-node strong { font-size: 8px; } + .node-need { left: 10px; top: 38px; } + .node-rules { right: 10px; top: 38px; } + .node-build { bottom: 38px; left: 10px; } + .node-proof { bottom: 38px; right: 10px; } + .map-node::after { width: 28px; } + .console-footer { gap: 10px; grid-template-columns: repeat(3, 1fr); padding: 0 14px; } + .console-wave { display: none; } + .question-band { padding: 74px 20px; } + .section { padding: 82px 20px; } + .benefits-grid, .partner-grid, .model-grid, .architecture-grid, .shared-runtime, .code-examples, .docs-grid { grid-template-columns: 1fr; } + .benefits-grid article { grid-column: auto; min-height: 220px; } + .partner-grid::before { display: none; } + .partner-grid article { min-height: 0; } .alignment-strip { align-items: stretch; flex-direction: column; text-align: center; } .alignment-strip b { transform: rotate(90deg); } - .technical-intro { padding: 64px 20px; } - .code-examples article:last-child { grid-column: auto; } - .code-examples pre { min-height: 0; } - .model-grid li { border-radius: 0 !important; min-height: 210px; } - .model-grid li:first-child { border-radius: var(--radius) var(--radius) 0 0 !important; } - .model-grid li:last-child { border-radius: 0 0 var(--radius) var(--radius) !important; } - .file-tree { padding: 18px; } - .file-tree div { grid-template-columns: 16px 1fr; padding-left: 10px; } - .file-tree div small { display: none; } - .architecture-card { padding: 26px 20px; } - .card-lede { min-height: 0; } .product-body { grid-template-columns: 42px 1fr; min-height: 340px; } .product-body aside { padding: 22px 12px; } .product-body aside span { width: 16px; } .board { overflow: hidden; padding: 20px 14px; } .columns { min-width: 520px; } - .start-band { gap: 35px; padding: 72px 20px; } + .technical-intro { padding: 76px 20px; } + .model-grid li { min-height: 240px; } + .file-tree { padding: 17px; } + .file-tree div { grid-template-columns: 16px 1fr; padding-left: 8px; } + .file-tree div small { display: none; } + .architecture-card { padding: 26px 20px; } + .code-examples article:last-child { grid-column: auto; } + .code-examples pre { min-height: 0; } + .docs-grid a { min-height: 270px; } + .start-band { gap: 34px; padding: 78px 20px; } .terminal pre { padding: 20px 16px; } .footer { align-items: start; grid-template-columns: 1fr; padding: 30px 20px; } .footer > a { justify-self: start; } From 33c8e594d97c72141317157f02022d06f96285e8 Mon Sep 17 00:00:00 2001 From: Adrian Besleaga Date: Sat, 15 Aug 2026 14:47:24 +0200 Subject: [PATCH 6/6] fix npm run check --- demo/ai-todo-assistant/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/demo/ai-todo-assistant/package.json b/demo/ai-todo-assistant/package.json index 802610a..38c295b 100644 --- a/demo/ai-todo-assistant/package.json +++ b/demo/ai-todo-assistant/package.json @@ -21,7 +21,7 @@ "test:integration": "tsx --test tests/mongo.integration.test.ts", "test:smoke": "node tests/built-server.smoke.mjs", "check:bundle": "node core/architecture/check-bundle-size.mjs", - "check": "npm run validate:architecture && npm run generate:contracts && npm run check:imports && tsc --noEmit" + "check": "npm run generate:contracts && npm run validate:architecture && npm run check:imports && tsc --noEmit" }, "devDependencies": { "@types/node": "^22.10.2",