PDR-011: Declarative configurator — spec, examples + working prototype - #32
PDR-011: Declarative configurator — spec, examples + working prototype#32kas1987 wants to merge 5 commits into
Conversation
…xamples Author PDR-011 proposing a data-driven "Build-a-ZELEX" configurator: a UI generated from a spec rather than hand-coded, where buyer selections form a JSON build document that resolves to a catalogued architecture, SKU, signature, family, and price. - db/configurator_schema.json: declarative axis model (body/head/components), derivations, validation rules, and a build-document JSON Schema. Option domains are extracted from existing db/ data; large sets referenced by source. - db/configurator_examples.json: three strong-data bodies (ZG170C, ZK168B, ZX172E) reverse-engineered into build documents with truthful resolved values, plus a bespoke failure case. - docs/pdr/PDR-011-declarative-configurator.md: method, data grounding, rules, and acceptance review. No live UI yet — proves the model round-trips real bodies before building it.
There was a problem hiding this comment.
Code Review
This pull request introduces a declarative configurator specification for ZELEX, including a data-driven schema, worked examples, and a detailed PDR. The code review identifies a few issues to resolve: correcting family premiums in the schema to align with db/body_profiles.json, fixing a logical inconsistency in the SLE skeleton version example where the delta unexpectedly alters the head and skin tone, and updating references to the non-existent family_taxonomy.json file to point to db/body_profiles.json instead.
Important
The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.
| "rule": "If resolved.sku matches a live variant, use its price (authoritative). Otherwise estimate: base_by_line × material_factor × (1 + family_premium). Mark estimated:true.", | ||
| "pricing_model": { | ||
| "base_by_line_usd": { "SLE 3.0": 1800, "Fusion": 3200, "I-Series": 3400, "K-Series": 3600 }, | ||
| "material_factor": { "Silicone": 1.0, "TPE": 0.85 }, |
There was a problem hiding this comment.
There is a discrepancy between the family premiums defined here and those in db/body_profiles.json. Specifically:
The Empressis set to0.30(30%) here, but is+40%(40%) indb/body_profiles.json(line 70).The Sculptis set to0.25(25%) here, but is+30%(30%) indb/body_profiles.json(line 84).
Since the note states that these premiums should mirror db/body_profiles.json, they should be updated to match.
| "material_factor": { "Silicone": 1.0, "TPE": 0.85 }, | |
| "family_premium": { "The Classic": 0.20, "The Muse": 0.25, "The Icon": 0.30, "The Siren": 0.35, "The Empress": 0.40, "The Sculpt": 0.30 }, |
| "id": "zxe215-zx172e", | ||
| "title": "SLE Icon — ZX172E (ZXE215_1 cast)", | ||
| "why_chosen": "Exercises the SLE-only axes (TPE material, skeleton_version) and the version/tone SKU suffix; exact-confidence Icon. Shows the same architecture priced two ways across skeleton versions.", | ||
| "build": { | ||
| "spec_version": "1.0.0", | ||
| "body": { "line": "SLE 3.0", "height_cm": 172, "cup": "E", "material": "TPE", "skeleton_version": "SLE 3.0" }, | ||
| "head": { "head_code": "ZXE215_1", "face_variant": "Standard", "head_tone_match": true }, | ||
| "components": { "skin_tone": "Tan", "breast_type": "Standard", "standing_feet": false, "fingers": false } | ||
| }, | ||
| "resolved": { | ||
| "body_code": "ZX172E", | ||
| "family": "The Icon", | ||
| "family_confidence": "exact", | ||
| "WHR": 0.608, | ||
| "BWR": 1.504, | ||
| "signature": { | ||
| "upper_bust": 86.5, "under_bust": 67, "waist": 57.5, "hip": 94.5, | ||
| "neck": 32.5, "shoulder_width": 38, "arm_length": 70.5, "hand_length": 19, | ||
| "leg_length": 84.5, "foot_length": 21, "thigh_circ": 48.5, "calf_circ": 34, | ||
| "weight_kg": 35.7 | ||
| }, | ||
| "sku": "ZXE215_1+ZX172E-Tan-Sle3.0", | ||
| "price": "2000.00", | ||
| "price_basis": "catalogued live variant (authoritative)", | ||
| "estimated": false | ||
| }, | ||
| "skeleton_version_alt": { | ||
| "note": "Same body, head, tone, but SLE 2.0 skeleton resolves a separate, lower-priced SKU — proving skeleton_version is a real price axis, not cosmetic.", | ||
| "build_delta": { "body.skeleton_version": "SLE 2.0" }, | ||
| "resolved_sku": "ZXE203_W1+ZX172E-White-Sle2.0", | ||
| "resolved_price": "1600.00" | ||
| }, | ||
| "source": { | ||
| "signature": "db/body_measurements.json#/bodies/ZX172E", | ||
| "family_whr_bwr": "db/body_profiles.json#/profiles (ZX172E)", | ||
| "sku_price": "db/live_variants.json (body=ZX172E, head=ZXE215_1, tone=Tan, Sle3.0)" | ||
| } |
There was a problem hiding this comment.
There is a logical inconsistency in the zxe215-zx172e example. The skeleton_version_alt block specifies a build_delta that only changes body.skeleton_version to "SLE 2.0". However, the resolved_sku is listed as "ZXE203_W1+ZX172E-White-Sle2.0", which also changes the head code (from ZXE215_1 to ZXE203_W1) and the skin tone (from Tan to White).
To make this a pure skeleton version delta, we can use a head/tone combination that actually exists in both SLE 2.0 and SLE 3.0 in the catalog, such as ZXE201_4 (Ulrica cast) or ZXE204_1 (Arin cast). Using ZXE201_4 allows the delta to resolve cleanly to "ZXE201_4+ZX172E-Tan-Sle2.0" without changing other axes.
"id": "zxe201-zx172e",
"title": "SLE Icon — ZX172E (ZXE201_4 cast)",
"why_chosen": "Exercises the SLE-only axes (TPE material, skeleton_version) and the version/tone SKU suffix; exact-confidence Icon. Shows the same architecture priced two ways across skeleton versions.",
"build": {
"spec_version": "1.0.0",
"body": { "line": "SLE 3.0", "height_cm": 172, "cup": "E", "material": "TPE", "skeleton_version": "SLE 3.0" },
"head": { "head_code": "ZXE201_4", "face_variant": "Standard", "head_tone_match": true },
"components": { "skin_tone": "Tan", "breast_type": "Standard", "standing_feet": false, "fingers": false }
},
"resolved": {
"body_code": "ZX172E",
"family": "The Icon",
"family_confidence": "exact",
"WHR": 0.608,
"BWR": 1.504,
"signature": {
"upper_bust": 86.5, "under_bust": 67, "waist": 57.5, "hip": 94.5,
"neck": 32.5, "shoulder_width": 38, "arm_length": 70.5, "hand_length": 19,
"leg_length": 84.5, "foot_length": 21, "thigh_circ": 48.5, "calf_circ": 34,
"weight_kg": 35.7
},
"sku": "ZXE201_4+ZX172E-Tan-Sle3.0",
"price": "2000.00",
"price_basis": "catalogued live variant (authoritative)",
"estimated": false
},
"skeleton_version_alt": {
"note": "Same body, head, tone, but SLE 2.0 skeleton resolves a separate, lower-priced SKU — proving skeleton_version is a real price axis, not cosmetic.",
"build_delta": { "body.skeleton_version": "SLE 2.0" },
"resolved_sku": "ZXE201_4+ZX172E-Tan-Sle2.0",
"resolved_price": "1600.00"
},
"source": {
"signature": "db/body_measurements.json#/bodies/ZX172E",
"family_whr_bwr": "db/body_profiles.json#/profiles (ZX172E)",
"sku_price": "db/live_variants.json (body=ZX172E, head=ZXE201_4, tone=Tan, Sle3.0)"
}| "id": "classify_family", | ||
| "produces": "body.family", | ||
| "inputs": ["signature.waist", "signature.hip", "signature.upper_bust"], | ||
| "rule": "WHR = waist/hip; BWR = upper_bust/waist; choose the family whose whr_range and bwr_range both contain the point (db/body_profiles.json#/families). On no exact containment, pick nearest range-center and label confidence near|loose (see db/family_taxonomy.json#/confidence_labels).", |
There was a problem hiding this comment.
The rule references db/family_taxonomy.json for confidence labels, but this file does not exist in the repository or the data_sources mapping. The confidence labels (exact, near, loose) are actually defined and used in db/body_profiles.json. This reference should be updated to avoid confusion.
| "rule": "WHR = waist/hip; BWR = upper_bust/waist; choose the family whose whr_range and bwr_range both contain the point (db/body_profiles.json#/families). On no exact containment, pick nearest range-center and label confidence near|loose (see db/family_taxonomy.json#/confidence_labels).", | |
| "rule": "WHR = waist/hip; BWR = upper_bust/waist; choose the family whose whr_range and bwr_range both contain the point (db/body_profiles.json#/families). On no exact containment, pick nearest range-center and label confidence near|loose (see db/body_profiles.json).", |
| - **`classify_family`** — `WHR = waist/hip`, `BWR = upper_bust/waist`; pick the | ||
| family whose ranges contain the point (`body_profiles.json#/families`); on no | ||
| containment, nearest-center with `near`/`loose` confidence | ||
| (`family_taxonomy.json`). **Prefer the stored classification** over a live |
There was a problem hiding this comment.
The PDR references family_taxonomy.json for confidence labels, but this file does not exist in the repository. The confidence labels are defined in body_profiles.json. This reference should be updated to body_profiles.json.
| (`family_taxonomy.json`). **Prefer the stored classification** over a live | |
| (body_profiles.json). **Prefer the stored classification** over a live |
Add a live "Build a ZELEX" configurator generated entirely from db/configurator_schema.json: a generic renderer walks groups[].axes[] to emit the kit's controls, and a resolver runs the four declared derivations + the validation rules in real time. Selections resolve to a body_code, family, measurement signature, SKU, and price — or an honest bespoke inquiry — and the page cross-links to the existing cast on the resolved architecture. - configurator.html: reuses assets/site.css tokens and ZX helpers (famColor, charCard, esc, nav/footer); no new design system. Resolver logic verified against db/ for ZG170C, ZK168B, ZX172E, and the bespoke ZK175K case. - validate-site.mjs: add configurator.html to KIT_PAGES so CI checks it. - Fix family_confidence for ZK168B: 'loose' (stored value), not 'near' — Muse contains neither axis in range. Corrected in examples + PDR. - PDR-011: status → Implemented; scope/acceptance updated to include the prototype.
…pathway Heads were the one axis with no structured model. Add scripts/build_heads.py that reverse-engineers the 189 head sculpts from products + variants + characters + movable-jaw overrides into db/heads.json (one record per sculpt: line, neck_class, MJ availability, skin tones, paired bodies, cast, representative image, price range). Mirrors body_measurements.json. Wire the configurator head picker to that data: - head_code now renders as an image gallery (ranked: pairs-this-body first, then cast/imaged); Movable Jaw gated to heads that actually offer it; resolve panel shows the chosen head thumbnail. Falls back to a dropdown if heads.json is absent. Head↔line coupling + interchangeability: the catalog sells heads line-locked (every head pairs one line), but the physical constraint is the neck connector, not the line. Spec-card necks cluster into two classes — slim (~27cm, K-Series) vs standard (~31-33cm, Fusion/I-Series/SLE). Add an "Interchangeable heads (cross-line)" toggle: on, the gallery shows every neck-compatible head across lines (cross-line ones flagged), a cross-line pick resolves to a custom/bespoke inquiry (never a fake SKU), and mismatched neck classes are blocked. Verified: no slim/standard leakage; standard body opens 186 heads, K-Series only 3. PDR-011 §8 documents the coupling + pathway; README pipeline + acceptance updated.
Neck data (interchangeability stops being a buried constant): - scripts/build_neck_compat.py → db/neck_compatibility.json: the single source of truth for neck classes. Explicit per-body neck_class, the classes/compatibility matrix, and the threshold in one reviewable place. Each body carries a neck_joint_mm slot (null) for the TRUE connector spec — fill it to replace the circumference-derived estimate with no code change. - build_heads.py imports the shared threshold/helpers from build_neck_compat (one definition, not two). configurator.html reads body neck_class from the data file (runtime calc is only a fallback) and uses the compatibility matrix for fit/validation instead of strict equality. Face & Finish (honest proposed layer): - No structured face-feature data exists in db/ (the only real per-head face attribute is movable-jaw, already modelled). So rather than fabricate per-head eye colours, add a Face & Finish group of provenance:proposed axes (eye colour, makeup, eyebrows, wig, freckles) — global options that compose as a custom finish (no SKU impact) and surface in the resolve panel. - heads.json gains a face_features slot (null) per head as the explicit backfill target; populate it to make these axes head-aware. - configurator state generalised to one bucket per schema group. PDR-011 §8/§9 + README pipeline + acceptance updated.
Add scripts/build_data_request.py → docs/data-requests/ (re-runnable): fill-in CSV templates + cover README for the two data points that close the estimate/proposed gaps. - neck-joint-request.csv (21 bodies): neck_joint_diameter_mm + type to replace the circumference-derived neck_class. - head-features-request.csv (189 heads, sorted priority-1 = has cast): per-head eye/makeup/eyebrows/wig/freckles defaults + availability for the face_features slot. Each row is pre-filled with known context; returned values load directly into db/neck_compatibility.json and db/heads.json with no schema change. PDR-011 §9 points to the templates.
What this is
A research-backed PDR + declarative spec + worked examples + a working live prototype for a "Build-a-ZELEX" configurator. The idea: a UI generated from a data spec (not hand-coded), where a buyer's selections form a JSON build document that resolves — through declared rules — to a catalogued body architecture, a SKU, a measurement signature, a Body Family, and a price (or an honest bespoke inquiry).
Files
db/configurator_schema.json— the declarative spec the UI is generated from: body / head / component axes,derivations(resolve body_code, classify family, resolve SKU + price),validationrules, and abuild_document_schema. Option domains are extracted from existingdb/data; large sets (163 heads) are referenced by source, not inlined.db/configurator_examples.json— three strong-data bodies reverse-engineered into build documents with truthful resolved values (transcribed fromdb/):ZG170C(I-Series Muse, exact) →GE52_2+ZG170C-Tan@ $3599 (+ Movable-Jaw alt)ZK168B(K-Series Muse, loose — off every family box) →KE03_1+ZK168B-1@ $3869.10ZX172E(SLE Icon, exact — exercises TPE + skeleton-version SKU suffix) →ZXE215_1+ZX172E-Tan-Sle3.0@ $2000body_code:null→ inquiry path)configurator.html— a working prototype: a generic renderer that walksgroups[].axes[]to emit the kit's controls, and a resolver that runs the fourderivations+validationrules live. Reusesassets/site.csstokens andZXhelpers (no new design system); cross-links to the existing cast on the resolved architecture. Resolver logic was sanity-checked againstdb/for all three examples + the bespoke case.docs/pdr/PDR-011-declarative-configurator.md— method, data grounding, the reversible derivation core, validation, scope, and acceptance review..github/scripts/validate-site.mjs— addsconfigurator.htmlto the validated KIT_PAGES.Key design decisions
body_code, not free composition — the configurator can't manufacture a body that doesn't exist, protecting the measurement-documented positioning.catalog/spec-card/derived/proposed; not-yet-in-db/options (standing feet, articulated fingers) are flaggedproposed.loosecase).Next step (not in this PR)
Backfilling the
proposedaxes indb/, an image gallery head picker, abuild_configurator.pypre-resolve step, and adding the page to the global nav.Validation
All new/changed JSON parses;
configurator.htmlpassesvalidate-site.mjs(inline script parses + references the shared kit); full site check green locally.https://claude.ai/code/session_01JFmZDEboYdpgMU11kWyqzB