v1.2.0:8 — tolerate decimal integers in AI output
Local models (Qwen via SparkControl, surfaced on the first SparkControl smoke test) sometimes emit a decimal where the AI-output schema expects an integer — e.g. a half-step "rpe": 7.5 or "reps": 8.0. Zod's .int() rejected these and failed the ENTIRE parse, so one stray decimal killed an otherwise good generation. Fix: a shared looseInt helper rounds a number to the nearest int before the .int() check, applied to every integer field in both the program and single-workout schemas (rpe, reps, sets, gear, order, durationSeconds, rest/week/day numbers). RPE/reps/sets are stored as integers downstream, so rounding is the correct landing. Transform-before-validate, so inferred types are unchanged. Parse-only; no schema/data change. 261 tests pass; built + sideloaded to immense-voyage.local (1.2.0:8, clean non-root launch). SparkControl now confirmed working end-to-end.
This commit is contained in:
@@ -116,13 +116,15 @@ Canonical publish path for this project: `~/.proof-of-work/publish.sh` (builds,
|
||||
|
||||
## Current state
|
||||
|
||||
Latest version is **1.2.0:7** — **SparkControl AI provider + base-URL footgun fix**. Adds a 6th provider, **SparkControl (local)** — the operator's own self-hosted local-inference gateway. OpenAI-compatible wire format (reuses `generateOpenAIStyle`), **keyless** on the LAN (`requireApiKey:false` → no `Authorization` header), reached over the **internal same-box StartOS address** `http://spark-control.startos:9999/v1` (plain HTTP — no TLS, no cert-skip; the public LAN interface is HTTPS w/ a self-signed cert we deliberately avoid). The Settings form **auto-detects the loaded vLLM model** via SparkControl's `/api/endpoints` (`app/api/ai/sparkcontrol/model`, admin-only + SSRF-guarded), mirroring Ollama auto-detect; $0 in the cost UI. Also fixes a **config footgun** (origin of this session): a custom base URL could ride along to a fixed-URL provider (claude/openai/gemini) whose form field is hidden, get stored, and be **silently ignored** (the provider hits its hardcoded endpoint) — both config write paths (`configs` POST + `[id]` PATCH) now null `baseUrl` for non-custom-URL providers and the form clears it on provider change. **No schema/data change** (`AIConfigProfile.provider` is a free-text column). Details: `docs/guides/ai-subsystem.md` → Provider abstraction (incl. the new "adding a provider" fan-out checklist). **Built + sideloaded** (`immense-voyage.local`, 2026-06-19, `master`) as `proof-of-work_x86_64.s9pk` (80M). tsc clean (app + packaging), lint clean (pre-existing only), **259 tests pass**, `next build` + s9pk build succeed. Registry empty, **publishing parked** (sideload-only via `make install`).
|
||||
Latest version is **1.2.0:8** — **decimal-tolerant AI parsing**, a fix-forward on the 1.2.0:7 SparkControl ship. **SparkControl is confirmed working end-to-end on-box** (smoke-tested with `RedHatAI/Qwen3.6-35B-A3B-NVFP4`: connected, streamed 9.7k in / 5.3k out, `$0`/FREE, model auto-detected). The smoke test surfaced one bug: local models emit decimals where the AI schema expected integers (a half-step `"rpe": 7.5`), and zod `.int()` failed the **entire** parse. Fix: a shared `looseInt` (`programSchema.ts`, used by `workoutSchema.ts`) rounds a number to the nearest int **before** the `.int()` check, applied to every integer field in both the program and single-workout schemas (rpe, reps, sets, gear, order, durationSeconds, rest/week/day numbers). Parse-only, types unchanged.
|
||||
|
||||
**1.2.0:7 (the SparkControl feature itself):** adds a 6th provider, **SparkControl (local)** — the operator's own self-hosted local-inference gateway. OpenAI-compatible wire format (reuses `generateOpenAIStyle`), **keyless** on the LAN (`requireApiKey:false` → no `Authorization` header), reached over the **internal same-box StartOS address** `http://spark-control.startos:9999/v1` (plain HTTP — no TLS/cert-skip; the public LAN interface is HTTPS w/ a self-signed cert we deliberately avoid). The Settings form **auto-detects the loaded vLLM model** via SparkControl's `/api/endpoints` (`app/api/ai/sparkcontrol/model`, admin-only + SSRF-guarded); $0 in the cost UI. Also fixed a **base-URL footgun**: a custom URL could ride along to a fixed-URL provider (claude/openai/gemini), get stored, and be silently ignored — both config write paths now null `baseUrl` for non-custom-URL providers and the form clears it on provider change. **No schema/data change** (`AIConfigProfile.provider` is free-text). Details: `docs/guides/ai-subsystem.md` → Provider abstraction (incl. the "adding a provider" fan-out checklist). **Built + sideloaded** (`immense-voyage.local`, 2026-06-19, `master`) as `proof-of-work_x86_64.s9pk` (80M). tsc clean (app + packaging), lint clean (pre-existing only), **261 tests pass**, `next build` + s9pk build succeed. Registry empty, **publishing parked** (sideload-only via `make install`).
|
||||
|
||||
**Design contract established this session (2026-06-19, committed `7fda9ce`, no UI code changed):** the `design/` folder now holds the durable contract — `DESIGN.md` (9-section brief) + `tokens.tokens.json` (DTCG) + `brand/palette.css` + `inspiration/` provenance. From a Case-B *document-as-is* extract of the as-built dark UI: **monochrome gym-brutalist** (`#0A0A0A` canvas, zinc-only neutral, white primary button, Bebas-uppercase/tracked headings, flat border-based depth), plus two owner calls — **red elevated to the single brand accent `#DC2626`** and a **two-tier radius** (4px controls / 8px containers). AGENTS.md carries the read-before-UI Design line; `ROADMAP.md` → **Design** holds the `design-checker` cleanup backlog (gray→zinc, green→emerald, yellow→amber, `rounded-md`→`rounded`, overlay-only shadows, and a shared `<Button>`). **Read `design/DESIGN.md` before any UI work.**
|
||||
|
||||
**Confirmed on-box (2026-06-19, via `start-cli`):** box runs `1.2.0:7`; launched `as nextjs` with no errors, "Ready in 221ms", and (correctly) **no migration ran** — this release adds no column. SparkControl itself is runtime-config (the operator must add a SparkControl config in Settings → AI to exercise it). Recent prior ships (1.2.0 line): **1.2.0:6** AI "today's workout"; **1.2.0:5** Gear replaces RPE for cardio; **1.2.0:4** watts as first-class set field.
|
||||
**Confirmed on-box (2026-06-19, via `start-cli`):** box runs `1.2.0:8`; launched `as nextjs` with no errors, "Ready in 221ms", and (correctly) **no migration ran** (neither :7 nor :8 adds a column). The operator added a SparkControl config and generated through it successfully (modulo the decimal bug now fixed in :8). Recent prior ships (1.2.0 line): **1.2.0:6** AI "today's workout"; **1.2.0:5** Gear replaces RPE for cardio; **1.2.0:4** watts as first-class set field.
|
||||
|
||||
**On-box follow-up (user action):** the live AI config is still the misconfigured one that triggered this session — `provider=gemini` with a LAN `baseUrl` the gemini provider ignores (so it silently hit Google's cloud and returned "Empty response"). The provider can't change on edit, so **delete that config and add a SparkControl one** (Settings → AI → Add config → SparkControl; URL auto-fills to `http://spark-control.startos:9999/v1`, model auto-detects, no key). Known bug (tracked in `ROADMAP.md` → Known bugs): the **1.2.0:2** Safari first-tap retry did NOT fix the mobile-Safari first-login failure — reproduced through 1.2.0:5 (the workout feature didn't touch auth), first tap shows "An unexpected error occurred", second tap works. Diagnosis captured; the fix is gated on one data point — the first failed request's error code from Safari Web Inspector (`-1005` → client delayed-retry; `502`/`503` → Node keep-alive tuning).
|
||||
**On-box follow-up:** the operator is now on the SparkControl config; the old misconfigured `gemini`+`baseUrl` profile (the empty-response trigger) is no longer active — delete it at leisure (cosmetic). Known bug (tracked in `ROADMAP.md` → Known bugs): the **1.2.0:2** Safari first-tap retry did NOT fix the mobile-Safari first-login failure — reproduced through 1.2.0:5 (the workout feature didn't touch auth), first tap shows "An unexpected error occurred", second tap works. Diagnosis captured; the fix is gated on one data point — the first failed request's error code from Safari Web Inspector (`-1005` → client delayed-retry; `502`/`503` → Node keep-alive tuning).
|
||||
|
||||
Working: workout logging, programs (manual + AI), multi-user, curated library, full AI subsystem (6 providers incl. **SparkControl**, multi-config, background generation, single-workout generation + refine, history detail, cost/duration, Ollama + SparkControl auto-detect, infinite-scroll exercise history).
|
||||
|
||||
|
||||
Reference in New Issue
Block a user