Follow-up to #22 (registry-drift shipped in v0.5.0).
The risk
vocabFromRegistry(reg) in prose.mjs builds the drift vocab by reaching into Zod internals:
const shape = verb.input?.shape ?? verb.input?._def?.shape?.() ?? {};
const opts = field?.options ?? field?._def?.entries ?? field?._def?.values;
…all inside a try { … } catch { /* verbspec version differences */ } that swallows failures.
If a zod/verbspec bump changes those internals, the vocab silently degrades to {help, version}. And because registryDrift flags an unknown --flag as error, a degraded vocab turns valid references (--catalog, --store, STORE=cas) in body/headline copy into false-positive errors — which would break the consumer site's gate (it keys on error). The swallowing catch hides the regression, and registryDrift has no guard for a near-empty vocab (the original spike no-op'd on flags.size <= 2).
Fix
Build the vocab from verbspec's public projection instead of Zod internals:
toMcpToolset(registry) / toMcpTool(verb).inputSchema → JSON Schema, where properties keys are the flags and properties[x].enum are the enum values, and the tool name is the verb id. This is the stable surface test.mjs already pins.
- Add a fail-safe guard: if the built vocab looks empty (only globals),
registryDrift returns [] (no findings) rather than false-positiving valid flags.
- Add a test that builds the vocab from the real registry (not just a hand-built vocab), so a future zod/verbspec bump is caught by CI instead of silently shipping false errors.
Spike
A zero-dep, runnable demo of the schema-based builder is in the linked PR (spikes/vocab-from-schema.mjs).
Acceptance
Follow-up to #22 (registry-drift shipped in v0.5.0).
The risk
vocabFromRegistry(reg)inprose.mjsbuilds the drift vocab by reaching into Zod internals:…all inside a
try { … } catch { /* verbspec version differences */ }that swallows failures.If a zod/verbspec bump changes those internals, the vocab silently degrades to
{help, version}. And becauseregistryDriftflags an unknown--flagaserror, a degraded vocab turns valid references (--catalog,--store,STORE=cas) inbody/headlinecopy into false-positive errors — which would break the consumer site's gate (it keys onerror). The swallowing catch hides the regression, andregistryDrifthas no guard for a near-empty vocab (the original spike no-op'd onflags.size <= 2).Fix
Build the vocab from verbspec's public projection instead of Zod internals:
toMcpToolset(registry)/toMcpTool(verb).inputSchema→ JSON Schema, wherepropertieskeys are the flags andproperties[x].enumare the enum values, and the toolnameis the verb id. This is the stable surfacetest.mjsalready pins.registryDriftreturns[](no findings) rather than false-positiving valid flags.Spike
A zero-dep, runnable demo of the schema-based builder is in the linked PR (
spikes/vocab-from-schema.mjs).Acceptance
_defaccess).registryDriftno-ops (never false errors).