Files
projectbrain-Public/packages/contracts/index.js
T

190 lines
19 KiB
JavaScript

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" } } },
} },
});