Files
curriculum-project-hub/hub/src/capacity/ceilings.ts
T
ChickenPige0n ab9dfad53a feat: org capacity policy admin surface
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.
2026-07-14 21:23:41 +08:00

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;
}