import { z } from "zod"; export const requestIdSchema = z.string().min(1); export const timestampSchema = z.string().datetime({ offset: true }); export const validationIssueSchema = z.object({ field: z.string().optional(), message: z.string().min(1), code: z.string().optional() }); export const apiMetaSchema = z.object({ requestId: requestIdSchema, timestamp: timestampSchema }); export const apiErrorEnvelopeSchema = z.object({ error: z.object({ code: z.string().min(1), message: z.string().min(1), validation: z.array(validationIssueSchema).optional() }), meta: apiMetaSchema, }); export const successEnvelopeSchema = (dataSchema = z.unknown()) => z.object({ data: dataSchema.refine((value) => value !== undefined, { message: "A success envelope must contain data" }), meta: apiMetaSchema, }); export const apiEnvelopeSchema = z.union([apiErrorEnvelopeSchema, successEnvelopeSchema()]); // Canonical lifecycle values are intentionally language-neutral. Frontends and // operational surfaces may translate them for people, but transport and // backend decisions must use the same finite vocabulary. export const canonicalLifecycleValues = Object.freeze([ "ready", "running", "waiting", "needs_user", "recovering", "failed", "done", ]); export const canonicalLifecycleSchema = z.enum(canonicalLifecycleValues); export const autonomyProfileSchema = z.enum(["conservative", "normal", "autonomous", "custom"]); export const autonomyProfileSettings = Object.freeze({ conservative: Object.freeze({ autoBrainstorm: false, maxBrainstormIterations: 2, maxConsecutiveFailures: 2, leaseMinutes: 20, maxWorkItemsPerRun: 3, maxRunMinutes: 20 }), normal: Object.freeze({ autoBrainstorm: true, maxBrainstormIterations: 5, maxConsecutiveFailures: 3, leaseMinutes: 30, maxWorkItemsPerRun: 8, maxRunMinutes: 45 }), autonomous: Object.freeze({ autoBrainstorm: true, maxBrainstormIterations: 5, maxConsecutiveFailures: 5, leaseMinutes: 45, maxWorkItemsPerRun: 12, maxRunMinutes: 55 }), custom: undefined, }); // Standard views must explain a blocker without making raw execution payloads // (SHAs, workflow names, leases, etc.) the primary product language. Both the // API portfolio read model and the web client use this same mapping. const normalizeOperationalCopy = (value) => value.replace(/\s+/g, " ").trim(); const containsTechnicalOperationalDetail = (value) => /\b(?:[a-f0-9]{7,64}|https?:\/\/|\.gitea\/|workflow|default branch|branch protection|pull request|\bpr\s*#?\d+|executor|lease|run \d+|plan \w{12,})\b/i.test(value); const firstOperationalSentence = (value) => { const sentence = normalizeOperationalCopy(value).split(/(?<=[.!?])\s+/)[0] ?? ""; return sentence.length > 220 ? `${sentence.slice(0, 217).trimEnd()}…` : sentence; }; export const summarizeHumanBlocker = (reason) => { const normalized = reason ? normalizeOperationalCopy(reason) : ""; if (!normalized) return "ProjectBrain heeft jouw beslissing of toegang nodig om veilig verder te gaan."; const lower = normalized.toLowerCase(); if (/governance|default branch|branch protection|workflow|pull request|\bpr\s*#?\d+/.test(lower)) return "ProjectBrain heeft jouw expliciete keuze nodig over een repositorywijziging voordat het veilig kan doorgaan."; if (/authori[sz]|permission|access|credential|token|owner/.test(lower)) return "ProjectBrain heeft jouw toestemming of toegang nodig om veilig verder te gaan."; if (/conflict|ambiguous|disagree|inconsistent/.test(lower)) return "ProjectBrain heeft jouw keuze nodig omdat betrouwbare projectinformatie elkaar tegenspreekt."; return containsTechnicalOperationalDetail(normalized) ? "ProjectBrain heeft jouw expliciete beslissing nodig voordat het deze stap verantwoord kan voortzetten." : firstOperationalSentence(normalized); }; export const summarizeTechnicalBlocker = (reason) => { const normalized = reason ? normalizeOperationalCopy(reason) : ""; if (!normalized) return "ProjectBrain onderzoekt de technische oorzaak en herstelt automatisch waar dat veilig kan."; const lower = normalized.toLowerCase(); if (/auth|credential|token|permission|access/.test(lower)) return "ProjectBrain herstelt een verbindings- of toegangsprobleem automatisch waar dat veilig kan."; if (/branch|pull request|workflow|repository/.test(lower)) return "ProjectBrain controleert de repositorytoestand opnieuw voordat het verdergaat."; return containsTechnicalOperationalDetail(normalized) ? "ProjectBrain verwerkt een technische blokkade. Details blijven beschikbaar onder technische diagnose." : firstOperationalSentence(normalized); }; export const userRoleSchema = z.enum(["owner", "member"]); export const authUserSchema = z.object({ id: z.string().min(1), username: z.string().min(3).max(80), role: userRoleSchema }); export const loginCredentialsSchema = z.object({ username: z.string().trim().min(3).max(80), password: z.string().min(12).max(256) }); export const rotatePasswordSchema = z.object({ currentPassword: z.string().min(12).max(256), newPassword: z.string().min(12).max(256) }).refine((value) => value.currentPassword !== value.newPassword, { message: "New password must differ from current password", path: ["newPassword"] }); export const authSessionDataSchema = z.object({ user: authUserSchema, csrfToken: z.string().min(20), expiresAt: timestampSchema.optional() }); export const entraPublicConfigSchema = z.object({ enabled: z.boolean(), provider: z.literal("microsoft-entra") }); export const entraLinkStatusSchema = z.object({ enabled: z.boolean(), linked: z.boolean(), displayName: z.string().optional(), preferredUsername: z.string().optional() }); export const idSchema = z.string().min(1).regex(/^[A-Za-z0-9-_]+$/); export const mutationResponseSchema = z.object({ success: z.boolean(), message: z.string().min(1), updatedEntityId: idSchema.optional(), createdEntityId: idSchema.optional(), affectedEntities: z.array(idSchema) }); export const captureCreateSchema = z.object({ type: z.string().min(1), content: z.string().trim().min(1), source: z.string().optional() }); export const operatingLogCreateSchema = z.object({ type: z.string().min(1), title: z.string().trim().min(1), summary: z.string().trim().min(1), linkedEntityIds: z.array(idSchema).optional(), source: z.string().optional(), importance: z.string().optional() }); export const apiPaths = Object.freeze({ auth: { login: "/api/auth/login", session: "/api/auth/session", logout: "/api/auth/logout", rotatePassword: "/api/auth/rotate-password", entraConfig: "/api/auth/entra/config", entraStart: "/api/auth/entra/start", entraCallback: "/api/auth/entra/callback", entraStatus: "/api/auth/entra/status", entraLinkStart: "/api/auth/entra/link/start", entraIdentity: "/api/auth/entra/identity", }, captureItems: (projectId = ":projectId") => `/api/projects/${projectId}/capture-items`, classifyCapture: (projectId = ":projectId", captureItemId = ":captureItemId") => `/api/projects/${projectId}/capture-items/${captureItemId}/classify`, sendCaptureToReview: (projectId = ":projectId", captureItemId = ":captureItemId") => `/api/projects/${projectId}/capture-items/${captureItemId}/send-to-review`, acceptReview: (projectId = ":projectId", reviewItemId = ":reviewItemId") => `/api/projects/${projectId}/review-queue/${reviewItemId}/accept`, materializeTruth: (projectId = ":projectId", reviewItemId = ":reviewItemId") => `/api/projects/${projectId}/review-queue/${reviewItemId}/materialize-truth`, createProjectUpdate: (projectId = ":projectId", truthItemId = ":truthItemId") => `/api/projects/${projectId}/accepted-truth/${truthItemId}/project-updates`, applyProjectUpdate: (projectId = ":projectId", projectUpdateId = ":projectUpdateId") => `/api/projects/${projectId}/project-updates/${projectUpdateId}/apply`, handoffBundles: (projectId = ":projectId") => `/api/projects/${projectId}/handoff-bundles`, codexTaskFromHandoff: (projectId = ":projectId", handoffBundleId = ":handoffBundleId") => `/api/projects/${projectId}/handoff-bundles/${handoffBundleId}/codex-task`, importCodexOutput: (projectId = ":projectId", codexTaskId = ":codexTaskId") => `/api/projects/${projectId}/codex-tasks/${codexTaskId}/import-output`, projectActivity: (projectId = ":projectId") => `/api/projects/${projectId}/activity`, projectActivityRuntime: (projectId = ":projectId") => `/api/projects/${projectId}/activity/runtime`, repositoryFindings: (projectId = ":projectId") => `/api/projects/${projectId}/findings`, repositoryFinding: (projectId = ":projectId", findingId = ":findingId") => `/api/projects/${projectId}/findings/${findingId}`, findingIssue: (projectId = ":projectId", findingId = ":findingId") => `/api/projects/${projectId}/findings/${findingId}/issue`, findingInvestigation: (projectId = ":projectId", findingId = ":findingId") => `/api/projects/${projectId}/findings/${findingId}/investigations`, findingSuppression: (projectId = ":projectId", findingId = ":findingId") => `/api/projects/${projectId}/findings/${findingId}/suppress`, findingAutomationPolicy: (projectId = ":projectId") => `/api/projects/${projectId}/finding-automation-policy`, portfolioControlPlane: "/api/portfolio/control-plane", portfolioRuntime: "/api/portfolio/runtime", portfolioSourceSync: "/api/portfolio/sources/sync-git", projectSourceSync: (projectId = ":projectId") => `/api/projects/${projectId}/sources/sync-git`, portfolioRepositories: "/api/portfolio/repositories", portfolioReconcile: "/api/portfolio/reconcile", portfolioAnalysisJobs: "/api/portfolio/analysis-jobs", portfolioAnalysisJob: (jobId = ":jobId") => `/api/portfolio/analysis-jobs/${jobId}`, portfolioAnalysisJobResults: (jobId = ":jobId") => `/api/portfolio/analysis-jobs/${jobId}/results`, portfolioAnalysisJobCancel: (jobId = ":jobId") => `/api/portfolio/analysis-jobs/${jobId}/cancel`, portfolioRepositoryExclude: (repositoryId = ":repositoryId") => `/api/portfolio/repositories/${repositoryId}/exclude`, portfolioRepositoryRestore: (repositoryId = ":repositoryId") => `/api/portfolio/repositories/${repositoryId}/restore`, workGraph: (projectId = ":projectId") => `/api/projects/${projectId}/work-graph`, workGraphReady: (projectId = ":projectId") => `/api/projects/${projectId}/work-graph/ready`, workGraphBlockers: (projectId = ":projectId") => `/api/projects/${projectId}/work-graph/blockers`, workGraphHistory: (projectId = ":projectId") => `/api/projects/${projectId}/work-graph/history`, workGraphClaim: (projectId = ":projectId") => `/api/projects/${projectId}/work-graph/claim`, workGraphReclaim: (projectId = ":projectId") => `/api/projects/${projectId}/work-graph/reclaim`, workGraphRelease: (projectId = ":projectId", nodeId = ":nodeId") => `/api/projects/${projectId}/work-graph/${nodeId}/release`, workGraphComplete: (projectId = ":projectId", nodeId = ":nodeId") => `/api/projects/${projectId}/work-graph/${nodeId}/complete`, workGraphDependencies: (projectId = ":projectId", nodeId = ":nodeId") => `/api/projects/${projectId}/work-graph/${nodeId}/dependencies`, definitionOfDone: (projectId = ":projectId") => `/api/projects/${projectId}/definition-of-done`, autonomySimulation: (projectId = ":projectId") => `/api/projects/${projectId}/autonomy/simulation`, projectAttention: (projectId = ":projectId") => `/api/projects/${projectId}/attention`, projectKnowledge: (projectId = ":projectId") => `/api/projects/${projectId}/project-knowledge`, projectKnowledgeConflicts: (projectId = ":projectId") => `/api/projects/${projectId}/project-knowledge/conflicts`, projectEvidenceGraph: (projectId = ":projectId") => `/api/projects/${projectId}/evidence-graph`, diagnostics: "/api/diagnostics", diagnosticsHistory: "/api/diagnostics/history", projectDiagnostics: (projectId = ":projectId") => `/api/projects/${projectId}/diagnostics`, projectPulse: (projectId = ":projectId") => `/api/projects/${projectId}/pulse`, portfolioPulse: "/api/portfolio/pulse", commandInterpret: "/api/commands/interpret", projectCommandInterpret: (projectId = ":projectId") => `/api/projects/${projectId}/commands/interpret`, portfolioViews: "/api/portfolio/views", portfolioView: (viewId = ":viewId") => `/api/portfolio/views/${viewId}`, portfolioPins: "/api/portfolio/pins", portfolioPin: (projectId = ":projectId") => `/api/portfolio/pins/${projectId}`, }); const openApiPath = (path) => path.replace(/:([A-Za-z][A-Za-z0-9]*)/g, "{$1}"); const operations = [ ["post", apiPaths.auth.login, "Authenticate owner or member"], ["get", apiPaths.auth.session, "Read the active session"], ["post", apiPaths.auth.logout, "Revoke the active session"], ["post", apiPaths.auth.rotatePassword, "Rotate owner credentials"], ["get", apiPaths.auth.entraConfig, "Read public Microsoft Entra availability"], ["get", apiPaths.auth.entraStart, "Start Microsoft Entra login"], ["get", apiPaths.auth.entraCallback, "Complete Microsoft Entra login"], ["get", apiPaths.auth.entraStatus, "Read linked Microsoft Entra identity status"], ["post", apiPaths.auth.entraLinkStart, "Start authenticated Microsoft Entra identity linking"], ["delete", apiPaths.auth.entraIdentity, "Unlink Microsoft Entra identity after password confirmation"], ["post", apiPaths.captureItems(), "Capture source material"], ["patch", apiPaths.classifyCapture(), "Classify captured source"], ["post", apiPaths.sendCaptureToReview(), "Queue captured source for review"], ["post", apiPaths.acceptReview(), "Accept a review decision"], ["post", apiPaths.materializeTruth(), "Materialize accepted project truth"], ["post", apiPaths.createProjectUpdate(), "Prepare a project update"], ["patch", apiPaths.applyProjectUpdate(), "Apply a project update"], ["post", apiPaths.handoffBundles(), "Create a controlled handoff"], ["post", apiPaths.codexTaskFromHandoff(), "Create a Codex task"], ["post", apiPaths.importCodexOutput(), "Import execution feedback"], ["get", apiPaths.projectActivity(), "Read durable project, delivery and repository activity"], ["get", apiPaths.projectActivityRuntime(), "Read compact live project, Gitea runner and analysis-worker activity"], ["get", apiPaths.portfolioRuntime, "Read platform-wide runner and worker activity"], ["post", apiPaths.portfolioSourceSync, "Synchronize safe Git sources for the portfolio"], ["post", apiPaths.projectSourceSync(), "Synchronize safe Git sources for one project"], ["get", apiPaths.repositoryFindings(), "List durable repository findings"], ["get", apiPaths.repositoryFinding(), "Read one repository finding"], ["post", apiPaths.findingIssue(), "Create or synchronize a finding issue"], ["post", apiPaths.findingInvestigation(), "Start a bounded finding investigation"], ["post", apiPaths.findingSuppression(), "Suppress a finding with a reason"], ["get", apiPaths.findingAutomationPolicy(), "Read closed-loop automation policy"], ["put", apiPaths.findingAutomationPolicy(), "Update closed-loop automation policy"], ["get", apiPaths.portfolioControlPlane, "Read the central portfolio control plane"], ["get", apiPaths.portfolioRepositories, "List durable repository onboarding state"], ["post", apiPaths.portfolioReconcile, "Preview or apply idempotent repository onboarding"], ["post", apiPaths.portfolioAnalysisJobs, "Queue a bounded portfolio analysis"], ["get", apiPaths.portfolioAnalysisJob(), "Read one portfolio analysis job"], ["get", apiPaths.portfolioAnalysisJobResults(), "Read a bounded portfolio analysis result page"], ["post", apiPaths.portfolioAnalysisJobCancel(), "Cancel a portfolio analysis job"], ["post", apiPaths.portfolioRepositoryExclude(), "Exclude a repository from automatic onboarding"], ["post", apiPaths.portfolioRepositoryRestore(), "Restore a repository for onboarding"], ["get", apiPaths.workGraph(), "Read the durable cross-domain project work graph"], ["get", apiPaths.workGraphReady(), "List ready cross-domain work"], ["get", apiPaths.workGraphBlockers(), "Inspect work graph blockers"], ["get", apiPaths.workGraphHistory(), "Read work graph audit history"], ["post", apiPaths.workGraphClaim(), "Claim the highest-value ready work atomically"], ["post", apiPaths.workGraphReclaim(), "Reclaim expired work leases"], ["post", apiPaths.workGraphRelease(), "Release a work lease"], ["post", apiPaths.workGraphComplete(), "Record work completion and evidence"], ["put", apiPaths.workGraphDependencies(), "Update durable work dependencies"], ["get", apiPaths.definitionOfDone(), "Evaluate project definition of done"], ["put", apiPaths.definitionOfDone(), "Save project completion requirements"], ["get", apiPaths.autonomySimulation(), "Simulate the current autonomous plan"], ["get", apiPaths.projectAttention(), "Read the deduplicated project attention inbox"], ["get", apiPaths.projectKnowledge(), "Read the unified project knowledge view"], ["get", apiPaths.projectKnowledgeConflicts(), "Read project truth conflicts and reconciliation provenance"], ["get", apiPaths.projectEvidenceGraph(), "Read requirement-to-runtime evidence relationships"], ["post", apiPaths.diagnostics, "Run safe platform diagnostics"], ["get", apiPaths.diagnosticsHistory, "Read recent diagnostic history"], ["post", apiPaths.projectDiagnostics(), "Run safe project diagnostics"], ["get", apiPaths.projectPulse(), "Read the concise project pulse"], ["get", apiPaths.portfolioPulse, "Read the concise portfolio pulse"], ["post", apiPaths.commandInterpret, "Interpret a natural-language control command"], ["post", apiPaths.projectCommandInterpret(), "Interpret a project control command"], ["get", apiPaths.portfolioViews, "List saved portfolio views"], ["post", apiPaths.portfolioViews, "Create a saved portfolio view"], ["patch", apiPaths.portfolioView(), "Update a saved portfolio view"], ["delete", apiPaths.portfolioView(), "Delete a saved portfolio view"], ["get", apiPaths.portfolioPins, "List per-user pinned projects"], ["put", apiPaths.portfolioPin(), "Pin or unpin a project for the current user"], ]; const openApiPaths = () => operations.reduce((paths, [method, path, summary]) => { const normalizedPath = openApiPath(path); paths[normalizedPath] = { ...(paths[normalizedPath] ?? {}), [method]: { summary, responses: { 200: { description: "Successful canonical envelope" }, 201: { description: "Created canonical envelope" }, 400: { description: "Validation error envelope" }, 401: { description: "Authentication required" }, 403: { description: "Authorization or CSRF denied" }, 409: { description: "State transition conflict" } } }, }; return paths; }, {}); export const buildOpenApiDocument = () => ({ openapi: "3.1.0", info: { title: "ProjectBrain API", version: "1.0.0", description: "Authenticated private-network API; generated from @projectbrain/contracts." }, servers: [{ url: "/" }], paths: openApiPaths(), components: { schemas: { ApiMeta: { type: "object", required: ["requestId", "timestamp"], properties: { requestId: { type: "string", minLength: 1 }, timestamp: { type: "string", format: "date-time" } } }, ApiError: { type: "object", required: ["error", "meta"], properties: { error: { type: "object", required: ["code", "message"], properties: { code: { type: "string" }, message: { type: "string" } } }, meta: { $ref: "#/components/schemas/ApiMeta" } } }, } }, });