Pattern gallery

Module settings: attribute normalization

A per-part-type table seeded from the file, each row's options fetched by the schema's own reference.

What this solves

Attribute normalization asks a different question in every app that has tried it: which attributes should a part type's raw catalog values fold into. A table is the easy part; seeding its rows from the file's own distinct part types and fetching each row's options at render time is what every app hand-rolled or skipped. Here the schema names both moves, x-seed-from for the rows and x-options-from with a per-row {row.part_type} placeholder for the fetch, so the console never writes bespoke grid code to get there.
Use it when
  • A forced single choice needs an explicit tick, not a default that is already correctspeedway's normalize vocabulary card, one real taxonomy option but unchecked until acknowledged, an explicit owner ruling (NewJobForm.tsx:816-830)
  • Per-record attribute assignment needs a large vocabulary plus a sense of how much of the record already qualifieswalmart-mvp's legacy TaxonomyColumnPicker computes a RequiredAttributes chip list per leaf from an asterisk marker convention (TaxonomyColumnPicker.tsx:375-402), the closest analog before this schema's async per-row multiselect

Rendered

Fixture data; check both themes.
Viewer
Attributes per part type0 rows
Part typeAttributes
Nothing to seed from yet

Coverage

Raw values found per part type
Waiting on the file's part types.

Coverage is read from the file; the run rewrites the raw values into the chosen attributes.

patterns/module-attribute-normalization/example.tsxCopy this into a fresh route and it renders as above.
"use client";
import { Button, Card, Progress, SchemaForm, type SchemaFormHandle, type XTier } from "@versable-git/ui";
import { useRef, useState } from "react";
import fixture from "../../../../../../docs/plan/65-schema-form-fixtures/attribute-normalization.params.schema.json";
import { FILE_CONTEXT, resolveModuleOptions } from "../../components/schema-form/examples/module-stubs";
type PerPartTypeRow = { part_type?: string; attributes?: string[] };
// A stand-in for the run's own coverage read: a real console derives this
// percent from the file's raw values (context["item.coverage(attributes.raw)"]
// feeds the same number into the form's per-row hint), this card just hashes
// the row's part type so every row shows a different, stable bar.
function coveragePercent(partType: string): number {
let sum = 0;
for (let i = 0; i < partType.length; i++) sum += partType.charCodeAt(i);
return 40 + (sum % 55);
}
function CoverageSummary({ value }: { value: Record<string, unknown> | undefined }) {
const rows = Array.isArray(value?.per_part_type) ? (value!.per_part_type as PerPartTypeRow[]) : [];
return (
<Card title="Coverage" subtitle="Raw values found per part type" noAnimate>
<div className="flex flex-col gap-3">
{rows.length === 0 ? (
<span className="text-base-content/65 text-sm">Waiting on the file&apos;s part types.</span>
) : (
rows.map((row, i) => (
<div key={row.part_type ?? i} className="flex flex-col gap-1">
<span className="text-sm">{row.part_type}</span>
<Progress value={coveragePercent(row.part_type ?? "")} label color="primary" size="sm" aria-label={`${row.part_type ?? "row"} coverage`} />
</div>
))
)}
<p className="text-base-content/65 text-xs">
Coverage is read from the file; the run rewrites the raw values into the chosen attributes.
</p>
</div>
</Card>
);
}
export function ModuleAttributeNormalization() {
const ref = useRef<SchemaFormHandle>(null);
const [value, setValue] = useState<Record<string, unknown>>();
const [tier, setTier] = useState<XTier>("customer");
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>
))}
</div>
<SchemaForm
ref={ref}
schema={fixture}
value={value}
onChange={setValue}
mode={reviewing ? "review" : "edit"}
tier={tier}
settings={{ module_version: "v3" }}
context={FILE_CONTEXT}
resolveOptions={resolveModuleOptions}
onEditSection={() => setReviewing(false)}
/>
<CoverageSummary value={value} />
<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:816-830the normalize taxonomy checkbox, deliberately unchecked despite one real option, an explicit owner ruling from 2026-07-31; the schema's x-acknowledge keeps the same unchecked-until-chosen rule
  • walmart-mvp/frontend/src/features/catalog/TaxonomyColumnPicker.tsx:375-402RequiredAttributes, a chip list computed per taxonomy leaf from a marker convention in the spec string; the legacy app's closest analog to a per-part-type attribute list, and both apps mark their attribute surfaces deprecated or crude in their own docs
  • docs/plan/65-schema-form-spec.md section 5.3the params shape this fixture commits, and the two vocabulary entries it introduces first: x-seed-from for the table's rows, x-hint-from for the per-row coverage line

App-specific: The fixture stubs three things a real console must supply: the file's distinct part types (context["item.distinct(part.type)"]), the per-row coverage hint x-hint-from reads (context["item.coverage(attributes.raw)"]), and the attribute options per part type (resolveOptions answering /attributes?part_type=...). The coverage card beside the form is app logic, not SchemaForm's: a real console reads the same raw-value coverage the run will act on, this fixture only fakes a percent from the row's own part-type string. The taxonomy list (/taxonomies) is a small closed set the module owns; the console never authors it.

@versable-git/ui · composites proven in the apps