summaryrefslogtreecommitdiffhomepage
path: root/src/features/named-agents/logic/validate.ts
blob: c25524ac63b7c371211d0d1161bebddbaec10a43 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
/**
 * Pure validation for Named Agent form fields — zero DOM, zero effects.
 *
 * A Named Agent is usable only when (plan §5.4): the name is nonempty + unique
 * case-insensitively, and a provider + model are selected. These pure helpers
 * drive the card UI's validity gating + inline messages; the backend remains
 * the authority (it 400s/conflicts on a duplicate or empty name / malformed
 * model identifier).
 */

/** Trim + collapse runs of whitespace inside a name (survey-style display). */
export function normalizeName(raw: string): string {
  return raw.trim().replace(/\s+/g, " ");
}

/**
 * A name-validation error message, or `null` when the name is valid. `otherNames`
 * are the OTHER agents' names for the case-insensitive uniqueness check (the
 * caller EXCLUDES the card's own last-persisted name / the card being edited).
 */
export function nameError(name: string, otherNames: readonly string[]): string | null {
  const normalized = normalizeName(name);
  if (normalized.length === 0) return "Name is required.";
  const lower = normalized.toLowerCase();
  for (const other of otherNames) {
    if (other.trim().toLowerCase() === lower) return "A named agent with this name already exists.";
  }
  return null;
}

/** True iff `name` is a unique, nonempty agent name among `otherNames`. */
export function isUsableName(name: string, otherNames: readonly string[]): boolean {
  return nameError(name, otherNames) === null;
}

/**
 * Whether a full model identifier is syntactically valid — a canonical
 * `<provider>/<model>` with a nonempty provider (the part before the first "/")
 * AND a nonempty model (the part after). A slashless or provider-only value is
 * invalid for a Named Agent (it must resolve to a concrete model).
 */
export function isUsableModel(full: string): boolean {
  const i = full.indexOf("/");
  if (i <= 0) return false; // no slash, or empty provider
  const model = full.slice(i + 1);
  return model.length > 0;
}