feat: phase 1 of AI pipeline hardening — single LLM client + tier-aware models

Implements phase 1 of AI_PIPELINE_HARDENING_PLAN.md. Every Anthropic call
now goes through one module that owns retry, timeout, abort, structured-
output parsing, schema validation, and best-effort call telemetry.

* src/lib/llm.js — single callLLM entry point. Resolves model per tier
  (fast / standard / reasoning) with admin:model legacy fallback for the
  standard tier; 60s default timeout via AbortController; balanced-brace
  JSON extraction; LLMHttpError, LLMTruncatedError, LLMOutputError, and
  LLMValidationError surface clearly distinct failure modes.
* src/lib/llmRetry.js — exponential backoff with full jitter, retries
  only on transient HTTP statuses, honours Retry-After up to 60s, never
  retries on AbortError.
* src/lib/llmSchemas.js — Zod schemas for every structured task plus
  normalizeHandbookResult (collapses legacy "executes" relations into
  the canonical "executed_by" vocabulary).
* src/lib/api.js — thin shim over callLLM so existing callers (extraction
  pipeline, learning, quiz, R42, knowledge graph) keep working unchanged.
* src/lib/__tests__/ — 32 Vitest cases covering parse paths, error
  surfaces, simulation mode, model resolution, and schema validation.
* src/pages/Admin/index.jsx — three model inputs (fast / standard /
  reasoning) replacing the single legacy field; legacy value falls back
  for the standard tier so existing overrides survive.

Adds Zod and Vitest, plus an "npm run test" script.

Also cleans up the pre-existing repo-wide ESLint failures so phase 1's
"npm run lint passes" acceptance criterion can be checked: drops unused
React imports across the JSX tree (React 19 JSX runtime auto-imports),
attaches cause to rethrown errors in the service modules, ignores
pb_migrations in the ESLint config (PocketBase JSVM globals), and
removes one dead handleCreateCustom function in Leren.jsx. A real
behaviour bug surfaced in Testen.jsx — the quiz timer captured a stale
finishQuiz via setInterval closure; now updated via finishQuizRef so the
timer always invokes the latest callback.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
RaymondVerhoef
2026-05-20 13:50:09 +02:00
parent db5bb854c3
commit 4a8dbee7df
36 changed files with 1612 additions and 233 deletions

View File

@@ -1,5 +1,3 @@
import React from 'react';
const Button = ({
children,
variant = 'primary',

View File

@@ -1,5 +1,3 @@
import React from 'react';
const Card = ({ children, className = '', hoverable = false, ...props }) => {
return (
<div

View File

@@ -3,7 +3,7 @@
* Shared component used by both the student Learn page and the Admin Content Manager.
* Renders all four learning modes: Article, Slides, Podcast, Infographic.
*/
import React, { useState, useEffect, useRef } from 'react';
import { useState, useEffect } from 'react';
import {
ChevronLeft, ChevronRight, BookOpen, Presentation,
CheckCircle, BarChart2

View File

@@ -1,5 +1,3 @@
import React from 'react';
/**
* Respellion brand mark — typeset directly from BallPill Light.
* Three states drive the middle glyph; braces always remain.

View File

@@ -1,5 +1,3 @@
import React from 'react';
const Tag = ({ children, variant = 'default', className = '', ...props }) => {
const variants = {
default: "bg-bg-warm text-fg",