From 3b8b41704510d2fa4fd89c54e48c5a2d08113dae Mon Sep 17 00:00:00 2001 From: Miguel de Benito Delgado Date: Sun, 27 Sep 2026 18:31:52 +0200 Subject: [PATCH] :construction: WIP Step 12.1, Improve case validation AI-assisted-by: muse-spark, glm-5.3 --- .../test/benches/render_wasm/scenes/core.cljs | 63 ++++++++++++++++--- .../benches/render_wasm/scenes/rects.cljs | 20 ++++-- 2 files changed, 67 insertions(+), 16 deletions(-) diff --git a/frontend/test/benches/render_wasm/scenes/core.cljs b/frontend/test/benches/render_wasm/scenes/core.cljs index 1de13c664f..03f33ecd00 100644 --- a/frontend/test/benches/render_wasm/scenes/core.cljs +++ b/frontend/test/benches/render_wasm/scenes/core.cljs @@ -63,10 +63,14 @@ [:dpr {:optional true} ::sm/positive-safe-number]]) (def schema:view + "Camera placement plus optional viewport override. All numbers are finite: + unbounded `number?` accepts Infinity, which JSON cannot carry and the + renderer cannot use. Bounds come from `::sm/safe-number`, whose min/max + also rejects NaN and infinities." [:map {:closed true} - [:scale [:and number? pos?]] - [:x number?] - [:y number?] + [:scale ::sm/positive-safe-number] + [:x ::sm/safe-number] + [:y ::sm/safe-number] [:viewport {:optional true} schema:viewport]]) (defn resolve-viewport @@ -360,19 +364,41 @@ ;; Projection ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +(defn- finite-number? + "True for numbers JSON preserves: rejects NaN and infinities, which + `number?` accepts but `JSON.stringify` turns into null." + [value] + (and (number? value) + (js/isFinite value))) + (defn serializable? "True when `value` is plain data the runner can hand to the browser and - store in results: nil, booleans, numbers, strings, keywords, and maps and - vectors thereof. Records fail: a record reaches the runner as a host - object, not plain data. Functions, atoms and host objects fail." + store in results. + + True for nil, booleans, finite numbers, strings, keywords, and vectors, and + maps with strings or keyword keys. + + NaN, infinities and non-string map keys fail: JSON does not carry them + losslessly. + + Records fail: a record reaches the runner as a host object, not plain data. + + Functions, atoms and host objects fail. + + Keywords are encoded with `encode-id`: the planned `app.common.json` converter + renders keyword values by unqualified name, so `:rects/load` would arrive as + `\"load\"`." [value] (cond - (or (nil? value) (boolean? value) (number? value) + (or (nil? value) (boolean? value) (finite-number? value) (string? value) (keyword? value)) true (and (map? value) (not (record? value))) - (every? (fn [[k v]] (and (serializable? k) (serializable? v))) value) + (every? (fn [[k v]] + (and (or (string? k) (keyword? k)) + (serializable? v))) + value) (vector? value) (every? serializable? value) @@ -380,6 +406,22 @@ :else false)) +(defn encode-id + "Lossless JSON-wire encoding of a keyword identity: `:rects/load` becomes + `\"rects/load\"`, `:fresh` becomes `\"fresh\"`. `decode-id` inverts it. + The runner (ticket 11) applies this pair at the JSON boundary; nothing + else may turn identities into bare `name` strings." + [id] + (when-not (keyword? id) + (throw (ex-info (str "wire identities are keywords, got: " (pr-str id)) + {:type ::invalid-case :id id}))) + (subs (str id) 1)) + +(defn decode-id + "Inverts `encode-id`: `\"rects/load\"` becomes `:rects/load`." + [s] + (keyword s)) + (defn project-case "Plain-data projection of a registered case: internal keys (`:ns`, `:run!`, `:body-source`) stripped, collection defaults applied." @@ -392,8 +434,9 @@ (update :preparation #(or % {:version 1})))) (defn check-collected-case! - "Validates a collected case descriptor and returns it unchanged. Rejects - anything that is not plain data: browser-side run functions must never + "Validates a collected case descriptor and returns it unchanged. + + Rejects anything that is not plain data: browser-side run functions must never reach the runner." [projected] (when-not (sm/validate schema:collected-case projected) diff --git a/frontend/test/benches/render_wasm/scenes/rects.cljs b/frontend/test/benches/render_wasm/scenes/rects.cljs index 82d6237874..3926cb2693 100644 --- a/frontend/test/benches/render_wasm/scenes/rects.cljs +++ b/frontend/test/benches/render_wasm/scenes/rects.cljs @@ -26,6 +26,7 @@ where `rtx` is the injected runtime and each operation contributes its plain-data identity to the collected case." (:require + [app.common.schema :as sm] [benches.render-wasm.scenes.builder :as sb :include-macros true] [benches.render-wasm.scenes.core :as core :include-macros true])) @@ -37,12 +38,19 @@ :max-size 100}) (def ^:private schema:params - [:map {:closed true} - [:count [:int {:min 1 :max 100000}]] - [:width pos?] - [:height pos?] - [:min-size pos?] - [:max-size pos?]]) + "Workload parameters. Sizes are finite (`pos?` accepts Infinity, which JSON + cannot carry and the generator cannot honor); `:min-size` above `:max-size` + is rejected so parameter sweeps cannot silently measure a reversed range." + [:and + [:map {:closed true} + [:count [:int {:min 1 :max 100000}]] + [:width ::sm/positive-safe-number] + [:height ::sm/positive-safe-number] + [:min-size ::sm/positive-safe-number] + [:max-size ::sm/positive-safe-number]] + [:fn {:error/message "min-size exceeds max-size"} + (fn [{:keys [min-size max-size]}] + (<= min-size max-size))]]) (defn- rect-defaults "Attribute generators for one rectangle of the workload."