summaryrefslogtreecommitdiffhomepage
path: root/src/features/named-agents/logic/normalize.ts
diff options
context:
space:
mode:
Diffstat (limited to 'src/features/named-agents/logic/normalize.ts')
-rw-r--r--src/features/named-agents/logic/normalize.ts68
1 files changed, 68 insertions, 0 deletions
diff --git a/src/features/named-agents/logic/normalize.ts b/src/features/named-agents/logic/normalize.ts
new file mode 100644
index 0000000..c1acec2
--- /dev/null
+++ b/src/features/named-agents/logic/normalize.ts
@@ -0,0 +1,68 @@
+/**
+ * Pure network-seam normalizers for the Named Agents API — zero DOM, zero
+ * effects. The composition root coerces the UNTYPED JSON it receives from
+ * `GET/POST/PUT/DELETE /named-agents` through these so a malformed / partial
+ * response can never crash the renderer. Mirrors the concurrency / heartbeat
+ * network-seam guard pattern.
+ */
+
+import type { NamedAgent, NamedAgentInput } from "./types";
+
+function isRecord(value: unknown): value is Record<string, unknown> {
+ return value !== null && typeof value === "object";
+}
+
+function asString(value: unknown): string | null {
+ return typeof value === "string" && value.length > 0 ? value : null;
+}
+
+/**
+ * Coerce one untrusted record into a typed `NamedAgent`, or `null` when it is
+ * malformed (missing `id`, `name`, or `model` — or a non-string `model`).
+ */
+export function normalizeNamedAgent(value: unknown): NamedAgent | null {
+ if (!isRecord(value)) return null;
+ const id = asString(value.id);
+ const name = asString(value.name);
+ const model = asString(value.model);
+ if (id === null || name === null || model === null) return null;
+ return { id, name, model };
+}
+
+/**
+ * Coerce an untrusted `GET /named-agents` body into a typed Named Agent list.
+ * When the list key is absent/malformed, returns `[]` (defensive — the renderer
+ * shows the empty state rather than crashing).
+ */
+export function normalizeNamedAgents(data: unknown): readonly NamedAgent[] {
+ if (!isRecord(data) || !Array.isArray(data.namedAgents)) return [];
+ const out: NamedAgent[] = [];
+ for (const entry of data.namedAgents as readonly unknown[]) {
+ const agent = normalizeNamedAgent(entry);
+ if (agent !== null) out.push(agent);
+ }
+ return out;
+}
+
+/**
+ * Coerce an untrusted `POST`/`PUT /named-agents` body (the canonical saved
+ * record) into a typed `NamedAgent`, or `null` when malformed. The composition
+ * root surfaces an error when this returns null on an ok response.
+ */
+export function normalizeNamedAgentRecord(data: unknown): NamedAgent | null {
+ if (!isRecord(data)) return null;
+ return normalizeNamedAgent(data.namedAgent);
+}
+
+/**
+ * Coerce an untrusted `NamedAgentInput` at the create/update boundary so only
+ * clean strings reach the wire. Non-string fields drop to "" (name nonempty is
+ * validated separately; model nonempty too). The caller still runs name/model
+ * validation so a malformed body never reaches the backend.
+ */
+export function toNamedAgentInput(name: unknown, model: unknown): NamedAgentInput {
+ return {
+ name: typeof name === "string" ? name.trim() : "",
+ model: typeof model === "string" ? model : "",
+ };
+}