forked from EduCraft/curriculum-project-hub
ab9dfad53a
ADR-0022 / Spec.System.Capacity pins layered capacity limits: a platform ceiling per dimension is unbreakable, and each organization may only set a lower `organizationLimit`. The effective limit is the minimum of the two (`LayeredLimit.effective`); dimensions with no org override fall back to the platform ceiling. No dimension may be unlimited (a ceiling must exist before an org limit can be set, `LayeredLimit.Valid`). Add the backend: the pinned 23 CapacityDimension set + labels (src/capacity/dimensions.ts), platform ceilings sourced from existing runtime env vars plus `HUB_CEILING_<DIMENSION>` (src/capacity/ceilings.ts), the OrganizationCapacityPolicy prisma model + migration, the getCapacityPolicy/setCapacityPolicy service enforcing LayeredLimit.Valid, and org-admin GET/PUT /api/org/:orgSlug/capacity-policy routes wired into the org route tree. Add the admin-web surface: CapacityDimension/CapacityPolicyView api client types, capacityPolicy/setCapacityPolicy methods, a `容量` nav entry, and a capacity page that lists every dimension with its platform ceiling (or `未配置` when unset), an org-limit input (disabled until a ceiling exists), and a live effective-value column. Saving sends the partial limits map; the service rejects values above the ceiling or for unconfigured dimensions.
79 lines
2.7 KiB
TypeScript
79 lines
2.7 KiB
TypeScript
/**
|
|
* ADR-0022 platform ceilings (spec `LayeredLimit.platformCeiling`).
|
|
*
|
|
* Platform ceilings are the unbreakable upper bounds; org policy may only
|
|
* configure lower limits (see `LayeredLimit.Valid`). Exact numeric values are
|
|
* `OPEN` per spec and calibrated by capacity testing — this module is the
|
|
* single place to wire them.
|
|
*
|
|
* Seven dimensions are sourced from existing runtime env vars
|
|
* (`HUB_HTTP_BODY_LIMIT_BYTES`, `HUB_MAX_FILES_PER_MESSAGE`, `HUB_MAX_FILE_BYTES`,
|
|
* `HUB_HTTP_REQUESTS_PER_MINUTE`, `HUB_AGENT_MAX_CONCURRENT_RUNS`,
|
|
* `HUB_AGENT_MAX_RUN_SECONDS`, `HUB_AGENT_MAX_TURNS`). The remaining dimensions
|
|
* are sourced from `HUB_CEILING_<DIMENSION>` env vars; until those are set the
|
|
* dimension has no configured ceiling and the policy UI surfaces it as
|
|
* "未配置" (cannot enforce a limit without a ceiling).
|
|
*/
|
|
import type { CapacityDimension } from "./dimensions.js";
|
|
|
|
function positiveIntEnv(name: string): number | undefined {
|
|
const raw = process.env[name];
|
|
if (raw === undefined || raw === "") return undefined;
|
|
const n = Number.parseInt(raw, 10);
|
|
return Number.isFinite(n) && n > 0 ? n : undefined;
|
|
}
|
|
|
|
const ENV_BACKED_CEILINGS: Partial<Record<CapacityDimension, string>> = {
|
|
requestBodySize: "HUB_HTTP_BODY_LIMIT_BYTES",
|
|
attachmentCount: "HUB_MAX_FILES_PER_MESSAGE",
|
|
fileSize: "HUB_MAX_FILE_BYTES",
|
|
requestRate: "HUB_HTTP_REQUESTS_PER_MINUTE",
|
|
agentConcurrency: "HUB_AGENT_MAX_CONCURRENT_RUNS",
|
|
runWallTime: "HUB_AGENT_MAX_RUN_SECONDS",
|
|
runTurns: "HUB_AGENT_MAX_TURNS",
|
|
};
|
|
|
|
const CEILING_DIMENSIONS: readonly CapacityDimension[] = [
|
|
"requestRate",
|
|
"requestBodySize",
|
|
"agentConcurrency",
|
|
"admissionQueueLength",
|
|
"admissionQueueWait",
|
|
"fileSize",
|
|
"attachmentCount",
|
|
"archiveExpansion",
|
|
"projectStorage",
|
|
"organizationStorage",
|
|
"memberCount",
|
|
"projectCount",
|
|
"teamCount",
|
|
"folderCount",
|
|
"sessionCount",
|
|
"runWallTime",
|
|
"runTurns",
|
|
"runToolCalls",
|
|
"toolWallTime",
|
|
"runOutputSize",
|
|
"processMemory",
|
|
"processCpu",
|
|
"processCount",
|
|
];
|
|
|
|
function envNameFor(dimension: CapacityDimension): string {
|
|
return ENV_BACKED_CEILINGS[dimension] ?? `HUB_CEILING_${dimension.toUpperCase()}`;
|
|
}
|
|
|
|
/**
|
|
* Returns the configured platform ceilings (only the dimensions currently
|
|
* wired via env). Unconfigured dimensions are absent — callers must surface
|
|
* them, not assume unlimited (spec `LayeredLimit` forbids unlimited ceilings).
|
|
*/
|
|
export function readPlatformCeilings(): Partial<Record<CapacityDimension, number>> {
|
|
const ceilings: Partial<Record<CapacityDimension, number>> = {};
|
|
for (const dimension of CEILING_DIMENSIONS) {
|
|
const value = positiveIntEnv(envNameFor(dimension));
|
|
if (value !== undefined) ceilings[dimension] = value;
|
|
}
|
|
return ceilings;
|
|
}
|