Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
106 changes: 5 additions & 101 deletions apps/operator-backend/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,13 +51,7 @@ import { analyzeRedlinePackageWithGemini } from "./vision/gemini_redline_package
import { buildEvidencePack } from "./evidence/evidence_pack.js";
import { maybePersistAutoTurnMemory } from "./memory/auto_turn_memory.js";
import { addProjectStandard, readProjectProfile } from "./memory/project_profile.js";
import {
createRequirement,
listRequirements,
resolveRequirements,
retireRequirement,
reviseRequirement
} from "./memory/requirements_store.js";
import { handleRequirementsHttpRoute } from "./memory/requirements_http_routes.js";
import { requiresMemoryAuthentication } from "./memory/requirements_route_policy.js";
import { createOpenAiClient, resolveOpenAiApiKey } from "./openai_client.js";
import { getDesktopComputerConfig, relayDesktopComputerResponse } from "./desktop_computer.js";
Expand Down Expand Up @@ -2635,100 +2629,10 @@ const server = http.createServer(async (req, res) => {
}
}

if (req.method === "GET" && url.pathname === "/memory/requirements") {
try {
const scopeKind = url.searchParams.get("scope_kind");
const scopeId = url.searchParams.get("scope_id");
const statusRaw = url.searchParams.get("status");
const status = statusRaw === "retired" || statusRaw === "all" ? statusRaw : "active";
const limit = Number.parseInt(url.searchParams.get("limit") ?? "200", 10);
const scope = scopeKind && scopeId ? { kind: scopeKind as any, id: scopeId } : undefined;
return writeJson(res, 200, { ok: true, requirements: listRequirements({ scope, status, limit }) });
} catch (e) {
return writeJson(res, 400, { ok: false, error: e instanceof Error ? e.message : String(e) });
}
}

if (req.method === "POST" && url.pathname === "/memory/requirements") {
const body = await readJson(req).catch(() => null);
try {
const parsed = body && typeof body === "object" ? body as any : {};
const saved = createRequirement({
scope: parsed.scope,
key: parsed.key,
text: parsed.text,
tags: parsed.tags,
effective_from: parsed.effective_from,
effective_until: parsed.effective_until,
supersedes_requirement_ids: parsed.supersedes_requirement_ids,
source: parsed.source ?? "api",
session_id: parsed.session_id,
actor_id: auth.principal?.user_id ?? null,
evidence: parsed.evidence
});
appendAuditLine({ type: "requirements.created", ts: new Date().toISOString(), requirement_id: saved.requirement.requirement_id, revision: saved.requirement.revision, principal: auth.principal ?? null });
return writeJson(res, 201, saved);
} catch (e) {
return writeJson(res, 400, { ok: false, error: e instanceof Error ? e.message : String(e) });
}
}

if (req.method === "POST" && url.pathname === "/memory/requirements/revise") {
const body = await readJson(req).catch(() => null);
try {
const parsed = body && typeof body === "object" ? body as any : {};
const saved = reviseRequirement({
requirement_id: parsed.requirement_id,
expected_revision: Number(parsed.expected_revision),
scope: parsed.scope,
key: parsed.key,
text: parsed.text,
tags: parsed.tags,
effective_from: parsed.effective_from,
effective_until: parsed.effective_until,
supersedes_requirement_ids: parsed.supersedes_requirement_ids,
source: parsed.source ?? "api.revise",
session_id: parsed.session_id,
actor_id: auth.principal?.user_id ?? null,
evidence: parsed.evidence
});
appendAuditLine({ type: "requirements.revised", ts: new Date().toISOString(), requirement_id: saved.requirement.requirement_id, revision: saved.requirement.revision, principal: auth.principal ?? null });
return writeJson(res, 200, saved);
} catch (e) {
const message = e instanceof Error ? e.message : String(e);
return writeJson(res, /Revision conflict/.test(message) ? 409 : 400, { ok: false, error: message });
}
}

if (req.method === "POST" && url.pathname === "/memory/requirements/retire") {
const body = await readJson(req).catch(() => null);
try {
const parsed = body && typeof body === "object" ? body as any : {};
const saved = retireRequirement({
requirement_id: parsed.requirement_id,
expected_revision: Number(parsed.expected_revision),
source: parsed.source ?? "api.retire",
session_id: parsed.session_id,
actor_id: auth.principal?.user_id ?? null,
evidence: parsed.evidence
});
appendAuditLine({ type: "requirements.retired", ts: new Date().toISOString(), requirement_id: saved.requirement.requirement_id, revision: saved.requirement.revision, principal: auth.principal ?? null });
return writeJson(res, 200, saved);
} catch (e) {
const message = e instanceof Error ? e.message : String(e);
return writeJson(res, /Revision conflict/.test(message) ? 409 : 400, { ok: false, error: message });
}
}

if (req.method === "POST" && url.pathname === "/memory/requirements/resolve") {
const body = await readJson(req).catch(() => null);
try {
const parsed = body && typeof body === "object" ? body as any : {};
const receipt = resolveRequirements({ scope_refs: parsed.scope_refs, query: parsed.query, at: parsed.at, max_results: parsed.max_results });
return writeJson(res, receipt.status === "resolved" ? 200 : 409, { ok: receipt.status === "resolved", receipt });
} catch (e) {
return writeJson(res, 400, { ok: false, error: e instanceof Error ? e.message : String(e) });
}
const requirementsRoute = await handleRequirementsHttpRoute({ method: req.method ?? "", url, actor_id: auth.principal?.user_id ?? null, read_body: () => readJson(req) });
if (requirementsRoute) {
if (requirementsRoute.audit) appendAuditLine({ ...requirementsRoute.audit, principal: auth.principal ?? null });
return writeJson(res, requirementsRoute.status, requirementsRoute.body);
}

if (req.method === "POST" && url.pathname === "/memory/project-profile/standards") {
Expand Down
133 changes: 133 additions & 0 deletions apps/operator-backend/src/memory/requirements_http_routes.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
import {
createRequirement,
listRequirements,
resolveRequirements,
retireRequirement,
reviseRequirement
} from "./requirements_store.js";

export type RequirementsHttpRouteResult = {
status: number;
body: unknown;
audit?: { type: string; ts: string; requirement_id: string; revision: number };
};

type RequirementsHttpRouteArgs = {
method: string;
url: URL;
actor_id: string | null;
read_body: () => Promise<unknown>;
};

function errorResult(error: unknown, revisionConflict = false): RequirementsHttpRouteResult {
const message = error instanceof Error ? error.message : String(error);
return { status: revisionConflict && /Revision conflict/.test(message) ? 409 : 400, body: { ok: false, error: message } };
}

export async function handleRequirementsHttpRoute(args: RequirementsHttpRouteArgs): Promise<RequirementsHttpRouteResult | null> {
const { method, url, actor_id, read_body } = args;
if (method === "GET" && url.pathname === "/memory/requirements") {
try {
const scopeKind = url.searchParams.get("scope_kind");
const scopeId = url.searchParams.get("scope_id");
const statusRaw = url.searchParams.get("status");
const status = statusRaw === "retired" || statusRaw === "all" ? statusRaw : "active";
const limit = Number.parseInt(url.searchParams.get("limit") ?? "200", 10);
const scope = scopeKind && scopeId ? { kind: scopeKind as any, id: scopeId } : undefined;
return { status: 200, body: { ok: true, requirements: listRequirements({ scope, status, limit }) } };
} catch (error) {
return errorResult(error);
}
}

if (method === "POST" && url.pathname === "/memory/requirements") {
const body = await read_body().catch(() => null);
try {
const parsed = body && typeof body === "object" ? body as any : {};
const saved = createRequirement({
scope: parsed.scope,
key: parsed.key,
text: parsed.text,
tags: parsed.tags,
effective_from: parsed.effective_from,
effective_until: parsed.effective_until,
supersedes_requirement_ids: parsed.supersedes_requirement_ids,
source: parsed.source ?? "api",
session_id: parsed.session_id,
actor_id,
evidence: parsed.evidence
});
return {
status: 201,
body: saved,
audit: { type: "requirements.created", ts: new Date().toISOString(), requirement_id: saved.requirement.requirement_id, revision: saved.requirement.revision }
};
} catch (error) {
return errorResult(error);
}
}

if (method === "POST" && url.pathname === "/memory/requirements/revise") {
const body = await read_body().catch(() => null);
try {
const parsed = body && typeof body === "object" ? body as any : {};
const saved = reviseRequirement({
requirement_id: parsed.requirement_id,
expected_revision: Number(parsed.expected_revision),
scope: parsed.scope,
key: parsed.key,
text: parsed.text,
tags: parsed.tags,
effective_from: parsed.effective_from,
effective_until: parsed.effective_until,
supersedes_requirement_ids: parsed.supersedes_requirement_ids,
source: parsed.source ?? "api.revise",
session_id: parsed.session_id,
actor_id,
evidence: parsed.evidence
});
return {
status: 200,
body: saved,
audit: { type: "requirements.revised", ts: new Date().toISOString(), requirement_id: saved.requirement.requirement_id, revision: saved.requirement.revision }
};
} catch (error) {
return errorResult(error, true);
}
}

if (method === "POST" && url.pathname === "/memory/requirements/retire") {
const body = await read_body().catch(() => null);
try {
const parsed = body && typeof body === "object" ? body as any : {};
const saved = retireRequirement({
requirement_id: parsed.requirement_id,
expected_revision: Number(parsed.expected_revision),
source: parsed.source ?? "api.retire",
session_id: parsed.session_id,
actor_id,
evidence: parsed.evidence
});
return {
status: 200,
body: saved,
audit: { type: "requirements.retired", ts: new Date().toISOString(), requirement_id: saved.requirement.requirement_id, revision: saved.requirement.revision }
};
} catch (error) {
return errorResult(error, true);
}
}

if (method === "POST" && url.pathname === "/memory/requirements/resolve") {
const body = await read_body().catch(() => null);
try {
const parsed = body && typeof body === "object" ? body as any : {};
const receipt = resolveRequirements({ scope_refs: parsed.scope_refs, query: parsed.query, at: parsed.at, max_results: parsed.max_results });
return { status: receipt.status === "resolved" ? 200 : 409, body: { ok: receipt.status === "resolved", receipt } };
} catch (error) {
return errorResult(error);
}
}

return null;
}
28 changes: 28 additions & 0 deletions apps/operator-backend/test/requirements_store.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import {
import { runWithRequestContext, type RequestPrincipal } from "../src/request_context.js";
import { requiresMemoryAuthentication } from "../src/memory/requirements_route_policy.js";
import { captureRequirementsResponseGuard, enforceRequirementsResponseGuard, formatRequirementsPromptBlockSafely } from "../src/memory/requirements_response_policy.js";
import { handleRequirementsHttpRoute } from "../src/memory/requirements_http_routes.js";
import { authenticateRequest } from "../src/auth.js";
import { maybeContinueAecLevelPowerPlanPilot, startAecLevelPowerPlanPilot } from "../src/deterministic/aec_level_power_plan_runtime.js";
import { AEC_SEMANTIC_TASK_V1_SCHEMA, type AecSemanticTaskV1 } from "../src/aec_semantic_task.js";
Expand Down Expand Up @@ -398,3 +399,30 @@ test("OpenAI requirements policy formats prompts and blocks a stale action respo
assert.match(guarded.assistant_message, /changed while this plan was being prepared/);
assert.equal(guarded.requirements_receipt?.applied[0]?.key, "tags.leaders");
});

test("requirements HTTP adapter creates, lists, and reports revision conflicts", async () => {
mkWorkspace();
const created = await handleRequirementsHttpRoute({
method: "POST",
url: new URL("http://localhost/memory/requirements"),
actor_id: "eli",
read_body: async () => ({ scope: scope("office", "bimtools"), key: "tags.text", text: "Use readable text." })
});
assert.equal(created?.status, 201);
assert.equal(created?.audit?.type, "requirements.created");
const requirement = (created?.body as any).requirement;
const listed = await handleRequirementsHttpRoute({
method: "GET",
url: new URL("http://localhost/memory/requirements?scope_kind=office&scope_id=bimtools"),
actor_id: "eli",
read_body: async () => null
});
assert.equal((listed?.body as any).requirements[0].requirement_id, requirement.requirement_id);
const conflict = await handleRequirementsHttpRoute({
method: "POST",
url: new URL("http://localhost/memory/requirements/revise"),
actor_id: "eli",
read_body: async () => ({ ...requirement, expected_revision: 0, text: "stale" })
});
assert.equal(conflict?.status, 409);
});
2 changes: 1 addition & 1 deletion scripts/backend_module_size_manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
{ "path": "brains/openai_brain.ts", "max_lines": 17625, "reason": "Legacy model orchestration monolith; durable requirements behavior is extracted to requirements_response_policy.ts, leaving five reviewed prompt/response wiring lines while provider and tool-loop seams continue to be extracted incrementally." },
{ "path": "benchmark/revit_workflows.ts", "max_lines": 10838, "reason": "Legacy benchmark workflow dispatcher; extract one independently tested workflow family per batch." },
{ "path": "benchmark/redline_routing_readiness.ts", "max_lines": 6285, "reason": "Legacy readiness report aggregator awaiting report-section extraction." },
{ "path": "index.ts", "max_lines": 4146, "reason": "Backend composition root; private hosted routes make the integration variant larger than public. Added page-range forwarding for bounded large-PDF analysis." },
{ "path": "index.ts", "max_lines": 4155, "reason": "Backend composition root; private hosted routes make the integration variant larger than public. Durable requirements routes are extracted to requirements_http_routes.ts, leaving nine reviewed authentication, adapter, and audit wiring lines alongside bounded large-PDF page-range forwarding." },
{ "path": "redline/redline_analyzer.ts", "max_lines": 3238, "reason": "Sheet classification and pure PDF annotation geometry are extracted; page analysis and image rendering remain coupled pending later bounded splits." },
{ "path": "deterministic/mep_route_redline.ts", "max_lines": 2914, "reason": "Deterministic MEP route resolver awaiting parsing and scoring extraction; one policy import aligns its PDF budget." },
{ "path": "redline/corpus_classifier.ts", "max_lines": 2876, "reason": "Corpus classification and promotion preparation remain coupled pending a focused split." },
Expand Down
Loading