Module settings: part type matching
What this solves
useState object rather than reach for a form engine. That temptation is where the discipline has to hold anyway: search_columnsstill needs the checklist-plus-sample-hint anatomy so a person isn't guessing which header holds a part name, and batchstill needs the tier gate so a customer never sees an internal call-size knob. A schema this short is not evidence the module doesn't need SchemaForm; it's evidence that even the smallest surface earns the same anatomy as the largest.- A module needs the smallest possible settings surface: an output field name, which columns to search, and a taxonomy choicespeedway's part-type-matching job config today: a single hardcoded taxonomy radio inside the create-job modal, never its own settings page (NewJobForm.tsx:412-421)
- A checklist's options are the file's own columns, with a data sample per option so a person isn't guessing which header holds the right valuesspeedway's Scrape module already does this shape for a different field, ScrapeFieldsPicker's fixed checkbox list (scrape-setup.tsx:120-217); walmart has no equivalent at setup time, going straight to a hierarchical picker at review time
Rendered
Fixture data; check both themes."use client";import { Button, Card, Chip, SchemaForm, type SchemaFormHandle, type XTier } from "@versable-git/ui";import { useRef, useState } from "react";import fixture from "../../../../../../docs/plan/65-schema-form-fixtures/part-type-matching.params.schema.json";import { FILE_CONTEXT, resolveModuleOptions } from "../../components/schema-form/examples/module-stubs";// Stub match rows for the aside: three sample titles with a faked matched// part type and confidence. The schema itself has no confidence field; a// live match step runs on the console's side once search_columns and// vocabulary are set, this is a demo affordance so the threshold below// visibly does something.const SAMPLE_MATCHES = [ { title: "Brake pad set", matched: "Brake pad", confidence: 92 }, { title: "Rotor", matched: "Rotor", confidence: 78 }, { title: "Cabin filter", matched: "Cabin filter", confidence: 54 },];function MatchPreview({ threshold }: { threshold: number }) { return ( <Card title="Match preview" subtitle="3 sample rows, stub matches" noAnimate> <div className="flex flex-col gap-2.5"> {SAMPLE_MATCHES.map((row) => { const below = row.confidence < threshold; return ( <div key={row.title} className="flex flex-col gap-0.5"> <div className="flex items-center justify-between gap-2"> <span className={below ? "text-base-content/65 text-sm" : "text-sm"}>{row.title}</span> <div className="flex items-center gap-2"> <Chip size="sm" variant={below ? "ghost" : "soft"} color={below ? "neutral" : "primary"}> {row.matched} </Chip> <span className={`text-xs tabular-nums ${below ? "text-base-content/65" : "text-base-content/70"}`}>{row.confidence}%</span> </div> </div> {below && <span className="text-base-content/65 text-xs italic">below threshold</span>} </div> ); })} </div> </Card> );}export function ModulePartTypeMatching() { const ref = useRef<SchemaFormHandle>(null); const [value, setValue] = useState<Record<string, unknown>>(); const [tier, setTier] = useState<XTier>("customer"); const [threshold, setThreshold] = useState(70); const [reviewing, setReviewing] = useState(false); const [result, setResult] = useState<string | null>(null); return ( <div className="flex flex-col gap-4"> <div className="flex flex-wrap items-center gap-2"> <span className="text-base-content/65 text-xs">Viewer</span> {(["customer", "admin"] as const).map((t) => ( <Button key={t} size="xs" variant={tier === t ? "solid" : "ghost"} color="primary" shade onClick={() => setTier(t)}> {t.charAt(0).toUpperCase() + t.slice(1)} </Button> ))} <span className="text-base-content/65 ml-3 text-xs">Confidence threshold</span> <input type="range" min={0} max={100} value={threshold} onChange={(e) => setThreshold(Number(e.target.value))} className="range range-primary range-sm w-32" /> <span className="text-base-content/70 text-xs tabular-nums">{threshold}%</span> </div> <SchemaForm ref={ref} schema={fixture} value={value} onChange={setValue} mode={reviewing ? "review" : "edit"} tier={tier} context={FILE_CONTEXT} resolveOptions={resolveModuleOptions} aside={<MatchPreview threshold={threshold} />} onEditSection={() => setReviewing(false)} /> <div className="flex items-center gap-2"> <Button size="sm" color="primary" onClick={() => { const out = ref.current?.submit(); setResult(out ? `Ready to run: ${Object.keys(out).length} settings` : "Fix the errors above"); }} > Validate </Button> <Button size="sm" variant="ghost" shade onClick={() => setReviewing((r) => !r)}> {reviewing ? "Back to edit" : "Review"} </Button> {result && <span className="text-base-content/65 text-xs">{result}</span>} </div> </div> );}Where it ships
speedway/app/components/NewJobForm.tsx:412-421normalizeUnacked: the vocab radio/checkbox for partType and normalize jobs, deliberately unchecked by default per an owner ruling requiring an explicit acknowledge; the closest real analogue to the fixture's vocabulary field, today a single taxonomy rather than a tenant/pcdb choice (moduleConfig.partType.vocabulary)speedway/app/routes/workspaces/jobs/scrape-setup.tsx:120-217the Scrape module's own field checklist (ScrapeFieldsPicker), checkboxes over a fixed list plus a free-text add; the closest existing checklist-over-known-names shape to search_columns, on a different modulewalmart-mvp/frontend/src/features/catalog/TaxonomyColumnPicker.tsx:43-403a Finder-style drill-down over about 385 taxonomy leaves, used to assign a part type at review time; spec 5.4 names the same shape as a V2 x-widget: tree candidate for the review editor, not this setup formdocs/plan/65-schema-form-spec.md §5.4the params shape this page renders: output_field, search_columns (checklist, x-options-from item.columns, x-hint-from item.sample(3)), vocabulary (segmented, tenant/pcdb), batch (admin, x-unit)
App-specific: The console supplies what this fixture stubs. resolveOptions here only answers the four paths module-stubs.ts fakes (models, taxonomies, attributes, product-types); it returns nothing for a real per-file column list, so search_columns and its sample hint work only because context carries item.columns and item.sample(3) directly. A live job would source both from the file the job actually holds, not a fixed array. The match preview beside the form (three sample rows, a faked matched part type, a confidence percent, a threshold slider) is entirely this page's invention: the schema in spec 5.4 carries no confidence field, and the real preview is the console's own match step, run against the job's real rows once search_columns and vocabulary are set, not a client-side stub reacting to a slider.