partforge 0.75.0 → 0.76.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -609,7 +609,12 @@ parameters: [
609
609
  ```
610
610
 
611
611
  Every control `key` must exist in `defaults`, or the control is silently dead —
612
- `control-key-not-in-defaults` is an error for exactly that reason.
612
+ `control-key-not-in-defaults` is an error for exactly that reason. Its value there must
613
+ be a **number, string or boolean** — a control writes one scalar, so a control bound to
614
+ an array, an object, `null` or `NaN` is dead in the same silent way
615
+ (`control-default-not-primitive`), and a host that saves panel settings back into
616
+ `defaults` cannot write that value either. A non-primitive `defaults` entry that no
617
+ control is bound to is fine: `defaults` also seeds `p` for `build()`.
613
618
 
614
619
  **Ids.** A section, a group and a preset may carry an `id`; the renderer keys its
615
620
  element, state and disclosure maps on ids, so they must be unique across the whole
@@ -1896,7 +1901,8 @@ about the definition as a whole use `""`.
1896
1901
  behave as authored — whether or not that shows up as a thrown exception. Some error
1897
1902
  findings do correspond to a runtime throw (`build-throws`, `verify-expect-throws`),
1898
1903
  but others catch **silent** wrongness: `missing-meta-title`, `part-view-unknown`,
1899
- `control-key-not-in-defaults`, `preset-key-not-in-defaults`, and
1904
+ `control-key-not-in-defaults`, `control-default-not-primitive`,
1905
+ `preset-key-not-in-defaults`, and
1900
1906
  `verify-unknown-subpart` all fire on parts that build, measure, and verify cleanly —
1901
1907
  a dead control that's silently unreachable, a view that renders nothing, or a
1902
1908
  `verify` expectation that's silently dropped so its gate never runs. That's still an
@@ -1913,7 +1919,8 @@ previously didn't; that's the fix working as intended, not a regression.
1913
1919
  `default-view-ambiguous` (warnings).
1914
1920
 
1915
1921
  **Parameter schema** — `features-requires-sliders`, `features-requires-on`,
1916
- `control-key-not-in-defaults`, `preset-key-not-in-defaults`, `mixed-section-shape`,
1922
+ `control-key-not-in-defaults`, `control-default-not-primitive`,
1923
+ `preset-key-not-in-defaults`, `mixed-section-shape`,
1917
1924
  `duplicate-preset-name`, `duplicate-node-id`, `select-options-missing`,
1918
1925
  `select-default-not-in-options`, `log-scale-needs-positive-min`,
1919
1926
  `when-key-not-in-defaults`, `when-unknown-operator`, `unknown-control-type` (errors);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "partforge",
3
- "version": "0.75.0",
3
+ "version": "0.76.0",
4
4
  "description": "Turn a declarative part definition into a parametric-CAD web app (three.js + Manifold/Replicad). Requires a Vite-based consumer.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -104,6 +104,22 @@ function collectPresetBundles(part) {
104
104
 
105
105
  const defaultKeys = (part) => new Set(Object.keys(part?.defaults ?? {}));
106
106
 
107
+ // What a control can actually edit: one finite scalar. Non-finite numbers are out
108
+ // with the rest — a slider cannot show NaN, and neither NaN nor Infinity survives
109
+ // a round trip through JSON or a source rewrite.
110
+ const isEditableValue = (v) =>
111
+ typeof v === "string" || typeof v === "boolean" || (typeof v === "number" && Number.isFinite(v));
112
+
113
+ // Name a value for a finding's message: the reader needs to know WHICH default is
114
+ // wrong and what it is, and `typeof []` ("object") tells them neither.
115
+ const describeValue = (v) => {
116
+ if (Array.isArray(v)) return "an array";
117
+ if (v === null) return "a null";
118
+ if (typeof v === "number") return `a ${Number.isNaN(v) ? "NaN" : String(v)}`;
119
+ if (v === undefined) return "an undefined";
120
+ return `a ${typeof v}`;
121
+ };
122
+
107
123
  // Walk one WhenCondition, calling onKey(key) for every param key it reads and
108
124
  // onOp(op) for every operator name it uses. allOf/anyOf/not recurse; a bare
109
125
  // `{ key: value }` entry counts as reading `key` with no operator to check.
@@ -191,6 +207,32 @@ export const SCHEMA_RULES = [
191
207
  `${path}.key`));
192
208
  },
193
209
  },
210
+ {
211
+ // A control edits ONE value, and the widgets read and write it as a scalar:
212
+ // a slider over an array renders NaN, a checkbox over an object writes
213
+ // `true` over it. It is dead in the same silent way control-key-not-in-defaults
214
+ // is — the part builds, the panel renders, the control does nothing usable —
215
+ // which is why this is an error rather than a warning.
216
+ //
217
+ // Only a key a CONTROL is bound to. `defaults` also seeds `p` for build(), so
218
+ // parking a table or a point list there for the build to read is legitimate
219
+ // authoring and none of lint's business.
220
+ id: "control-default-not-primitive",
221
+ run: ({ part }) => {
222
+ // Same guard as control-key-not-in-defaults: bail only when `defaults`
223
+ // isn't a plain object at all (missing-defaults already reports that).
224
+ if (!isPlainObject(part?.defaults)) return [];
225
+ const defaults = part.defaults;
226
+ return collectDescriptors(part)
227
+ .filter(({ container }) => !container)
228
+ .filter(({ d }) => typeof d.key === "string" && Object.hasOwn(defaults, d.key))
229
+ .filter(({ d }) => !isEditableValue(defaults[d.key]))
230
+ .map(({ d, path }) => err("control-default-not-primitive",
231
+ `control "${d.key}" is bound to ${describeValue(defaults[d.key])} default`,
232
+ `Give "${d.key}" a number, string or boolean default — a control writes one scalar, so it cannot edit this, and a host that saves panel settings back into \`defaults\` cannot write it either.`,
233
+ `${path}.key`));
234
+ },
235
+ },
194
236
  {
195
237
  id: "preset-key-not-in-defaults",
196
238
  run: ({ part }) => {