9.5 KiB
Validation Gates
The orchestrator runs only cross-skill validations — checks that span the two CLTs, the widget, and the renderer. Widget-bundle-internal checks (schema parses, root keys, leaf types, {!$attrs.X} resolution, .uiwidget-meta.xml well-formedness, <UiWidgetBundle> root, widget-type) are owned by platform-widget-generate and run in its own self-validation.
Run every gate below. If a hard gate fails, fix and re-run before reporting success. Warn gates are advisory.
Shell note: run each command verbatim and reason about its printed output. Do NOT capture into shell variables with $(…), do NOT use process substitution <(…), do NOT use brace expansion. Vibes' safe-shell filter blocks those patterns and prompts for manual approval even in Bypass mode. See Hard Rule 11 in the SKILL.md.
Hard — block on failure
1. clt-reference-integrity
Confirms the envelope→response typing the renderer depends on.
-
Both CLTs parse:
jq . <pkgDir>/lightningTypes/<responseCLT>/schema.json > /dev/null && echo "RESPONSE_PARSE: ok" || echo "RESPONSE_PARSE: FAIL"jq . <pkgDir>/lightningTypes/<toolCLT>/schema.json > /dev/null && echo "ENVELOPE_PARSE: ok" || echo "ENVELOPE_PARSE: FAIL" -
Envelope
outputValuesreferences the response CLT. Print the value and compare in reasoning:jq -r '.properties.outputValues["lightning:type"]' <pkgDir>/lightningTypes/<toolCLT>/schema.jsonExpected:
c__<responseCLT>. Match →REFERENCE: ok; elseREFERENCE: FAIL (got <actual>, expected c__<responseCLT>). -
Neither CLT carries a forbidden keyword. Print any hits (empty output = clean):
jq 'paths | select(.[-1] == "$schema" or .[-1] == "items")' <pkgDir>/lightningTypes/<responseCLT>/schema.jsonjq 'paths | select(.[-1] == "$schema" or .[-1] == "items")' <pkgDir>/lightningTypes/<toolCLT>/schema.jsonAny output →
KEYWORDS: FAIL (<path>); empty →KEYWORDS: ok. -
Nested-object response fields (if any) use
@apexClassType, never a bare object. For every response CLT property that is not a primitive leaf, print itslightning:type:jq -r '.properties | to_entries[] | select(.value["lightning:type"] == null or (.value["lightning:type"] | test("^lightning__") | not)) | "\(.key): \(.value["lightning:type"])"' <pkgDir>/lightningTypes/<responseCLT>/schema.jsonEvery printed entry must match
@apexClassType/<ns>__<OuterClass>$<InnerClass>. A bare{"type":"object"}(nolightning:type, or alightning:typeoflightning__objectTypeinlined on a property rather than the CLT root) →NESTED_TYPE: FAIL (<key>: <actual>). All match or no such properties exist →NESTED_TYPE: ok.
Result: all ok → pass. Otherwise fail (<first failing check>).
Failure → fix: the renderer's {!$attrs.outputValues.<field>} paths cannot resolve unless outputValues is typed to the response CLT. Fix the envelope CLT's outputValues.lightning:type to c__<responseCLT>, ensure the response CLT exists, and remove any $schema / items keywords.
2. renderer-wires-widget
Confirms the envelope CLT's default renderer assigns the widget and binds every widget property through the nested outputValues path.
-
File exists at the bundle root and parses (NOT
lightningDesktopGenAi/):jq . <pkgDir>/lightningTypes/<toolCLT>/renderer.json > /dev/null && echo "PARSE: ok" || echo "PARSE: FAIL" -
Definition points at this widget:
jq -r '.renderer.componentOverrides["$"].definition' <pkgDir>/lightningTypes/<toolCLT>/renderer.jsonExpected:
@widget/c/<widgetName>. Match →DEFINITION: ok; elseDEFINITION: FAIL (got <actual>). -
Attribute keys cover every widget schema property. Print both lists; compare in reasoning:
echo "SCHEMA_KEYS (expected):" jq -r '.properties.attributes.properties | keys[]' <pkgDir>/uiWidgets/<widgetName>/schema.json | sort -uecho "RENDERER_KEYS (actual):" jq -r '.renderer.componentOverrides["$"].attributes | keys[]' <pkgDir>/lightningTypes/<toolCLT>/renderer.json | sort -uSame set →
ATTRIBUTES: ok. Keys in SCHEMA not in RENDERER →ATTRIBUTES: FAIL (missing: <list>). Keys in RENDERER not in SCHEMA →ATTRIBUTES: FAIL (extra: <list>). -
Each binding uses the nested
outputValuespath. Dump the map and inspect each entry:jq '.renderer.componentOverrides["$"].attributes' <pkgDir>/lightningTypes/<toolCLT>/renderer.jsonFor every key
K, the value MUST equal{!$attrs.outputValues.K}exactly (nested path, matching key, no whitespace) — unlessKis a leaf of a nested-object response field (per the response CLT's@apexClassTypeproperties, seeclt-reference-integritycheck 4), in which case it MUST equal{!$attrs.outputValues.<objectField>.K}(three segments:outputValues, the object field, the leaf). All match their expected form →BINDINGS: ok. A flat{!$attrs.K}(missingoutputValues.) or a one-level binding for a nested-object leaf (missing the<objectField>.segment) is a FAIL — these are the most common mistakes in this flow. ReportBINDINGS: FAIL (<key>: got <value>, expected <expected>).
Result classification:
- All checks pass →
pass - File missing / at wrong path / invalid JSON →
fail (renderer.json missing, mislocated, or invalid — must be at lightningTypes/<toolCLT>/renderer.json) - Definition mismatch →
fail (definition does not point at widget: got <actual>) - Coverage mismatch →
fail (missing bindings: <list>)orfail (extra bindings: <list>) - Flat or malformed binding →
fail (binding for <key> is not nested under outputValues: <actual>)
Failure → fix: without correct nested wiring the widget either ships dead (no definition) or renders empty (flat bindings resolve against the envelope root, where the payload fields do not exist). Author the renderer per references/two-clt-modeling.md.
Warn — advisory
field-trace
Enforces: no invented widget fields (subset rule) and no silent omission of response fields.
INVOCABLE_FIELDS and WIDGET_PROPS are labels in the printed output, NOT shell variables. Do NOT assign with $(…).
-
Extract the authoritative payload field names, using the same source chosen in Phase 2:
actionsource (preferred) — read the Actions REST APIoutputs(already excludes inputs/helpers):echo "INVOCABLE_FIELDS:" sf api request rest '/services/data/v<APIVER>/actions/custom/apex/<ActionApiName>' -o <org> \ | jq -r '.outputs[].name' | sort -uapexsource — grep the response class (scope to the response class block only; exclude the request class and private helpers):echo "INVOCABLE_FIELDS:" grep -A1 '@InvocableVariable' <pkgDir>/classes/<ClassName>.cls \ | grep -oE '(public|global)\s+[A-Za-z0-9_<>,\s]+\s+[a-zA-Z_][a-zA-Z0-9_]*\s*;' \ | sed -E 's/.*\s([a-zA-Z_][a-zA-Z0-9_]*)\s*;/\1/' \ | sort -uIf grep misses multi-line annotations, read the
.clswith the Read tool and list fields manually. For thesamplesource, use theoutputValueskeys instead.Nested-object fields (per
references/mcp-tool-output-discovery.md"Nested-object payload fields") expand before comparison. A field resolved to@apexClassType/<ns>__<OuterClass>$<InnerClass>is not itself compared againstWIDGET_PROPS— the widget flattens to that class's own leaf fields, never the object field. Replace that field name inINVOCABLE_FIELDSwith its referenced class's leaf field names (its own@InvocableVariable/public members) before running the diff in step 3. Note the substitution in the printed output, e.g.INVOCABLE_FIELDS (expanded): flightInfo → flightId, origin, destination, departureTime, arrivalTime, price. -
Extract widget schema property keys:
echo "WIDGET_PROPS:" jq -r '.properties.attributes.properties | keys[]' <pkgDir>/uiWidgets/<widgetName>/schema.json | sort -u -
PRINT both lists in the gate report (not just an assertion):
INVOCABLE_FIELDS: accountName, accountIndustry, contactCount, status, ... WIDGET_PROPS: accountName, accountIndustry, contactCount, ... INVENTED (widget − invocable): <empty> OMITTED (invocable − widget): status, ... -
Result classification:
INVENTEDnon-empty → fail (subset rule violated).fail (invented: <list>).OMITTEDnon-empty AND every omitted field is in the Phase 3Properties omitted:→ pass.OMITTEDnon-empty AND any omitted field is NOT inProperties omitted:→ warn (silent omission).warn (silent omission: <list>), surface before the summary.- Both empty → pass.
Reporting pass without printing the two lists is a hard violation — report not run instead.
Direction of the subset rule
The widget schema.json and the response CLT properties are a subset of the response @InvocableVariable fields.
- No invented fields (hard via
field-trace). The widget must not introduce properties the response class does not expose. - No silent omissions (warn). The widget MAY omit response fields, but every omission must appear in the Phase 3
Properties omitted:section with a rationale.
Reporting
Phase 6 must list each gate's result by name: pass, fail (<reason>), warn (<reason>), or not run. Do not summarize as "all passed".