Skip to content

Commit df8ebe3

Browse files
committed
docs: record exhaustive registry bridge research in development plan
Documents all 13 investigated approaches to obtaining host's globalRegistry, Zod v4 internals (how descriptions are stored/read), and why the dynamic import approach was chosen as the production-viable solution.
1 parent 3a05e83 commit df8ebe3

1 file changed

Lines changed: 85 additions & 0 deletions

File tree

.vibe/development-plan-fix-opencode-plugin-parameter-descriptions.md

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,91 @@ The `tool.definition` hook bridges registries:
8383
- `toJSONSchema({ metadata: pluginRegistry })`: works but can't change OC's hardcoded call
8484
- Dynamic import approach: cleanest workable solution for production
8585

86+
### All Investigated Approaches to Get Host's globalRegistry
87+
88+
The core challenge: from within the plugin, we needed a reference to OpenCode's `globalRegistry` object (a `$ZodRegistry` instance). All approaches tried:
89+
90+
**1. Static import of `'zod'` (plugin's own copy)**
91+
- `import { globalRegistry } from 'zod'` → resolves to plugin's zod 4.3.6 registry (wrong)
92+
- Even after moving to peerDep, monorepo still has plugin/node_modules/zod@4.3.6 (pulled by another workspace pkg)
93+
94+
**2. Dynamic import of `'zod'` (production-only fix)**
95+
- `await import('zod')` from plugin code → also resolves to plugin's local zod in dev/monorepo
96+
- In **production** (no local node_modules/zod), this correctly resolves to host's zod ✅
97+
- Chosen approach — acceptable since dev uses known setup
98+
99+
**3. `_zod.bag` property on schema**
100+
- Hypothesis: descriptions stored in `_zod.bag` (a per-instance metadata bag)
101+
- Result: `_zod.bag` is always `{}` for described schemas — NOT used for descriptions
102+
103+
**4. `_zod.parent` trick**
104+
- `fieldSchema._zod.parent = ocDummySchema; ocRegistry.add(ocDummySchema, { description })`
105+
- `$ZodRegistry.get()` inherits from parent: `{ ...parentMeta, ...schemaOwnMeta }`
106+
- Result: `toJSONSchema` treats `_zod.parent` as a `$ref` clone relationship, outputs only `{ description }` with NO type info — field type entirely lost ❌
107+
108+
**5. `toJSONSchema({ metadata: pluginRegistry })` option**
109+
- `JSONSchemaGenerator` accepts `params?.metadata` to override the default `globalRegistry`
110+
- `z.toJSONSchema(parameters, { metadata: pluginRegistry })` — WORKS in isolation ✅
111+
- Problem: OC's `prompt.ts:406` call is hardcoded as `z.toJSONSchema(item.parameters)` — we can't inject the option ❌
112+
113+
**6. `$ZodRegistry.prototype.add` temporary monkey-patch**
114+
- Patch the prototype's `add` method; call `parameters.describe("probe")`; `this` inside `add` = the actual registry
115+
- WORKS in isolation ✅ (confirmed in test)
116+
- Problem: to get `$ZodRegistry.prototype`, need a `$ZodRegistry` instance first (circular)
117+
- `$ZodRegistry` is exported from `zod/v4/core`, but that resolves to plugin's zod in dev
118+
119+
**7. Cross-instance `$ZodRegistry.prototype` patch**
120+
- Plugin's `$ZodRegistry.prototype` vs OC's `$ZodRegistry.prototype`**different objects** (different module instances)
121+
- Patching plugin's prototype does NOT affect OC's globalRegistry ❌
122+
123+
**8. Extract registry via `parameters.describe()` closure**
124+
- `describe()` is a closure: `(desc) => { core.globalRegistry.add(clone, {description}); return clone }`
125+
- `core.globalRegistry` is captured in closure — cannot be extracted from outside
126+
- Tried: wrapping `parameters.describe`, using Proxy, inspecting closure variables — all failed
127+
128+
**9. `parameters.register(fakeReg, meta)``fakeReg.add(schema, meta)`**
129+
- `register(reg, meta)` just calls `reg.add(inst, meta)` with whatever `reg` we pass
130+
- We can intercept our own fake `reg.add` but that doesn't give us the HOST registry
131+
- Useful for writing TO a registry we provide, not for discovering the host's ❌
132+
133+
**10. `WeakMap.prototype.set` monkey-patch**
134+
- `$ZodRegistry._map` is a `WeakMap`; `add()` calls `this._map.set(schema, meta)`
135+
- Patching `WeakMap.prototype.set` could intercept the write, but we'd get the WeakMap, not the registry
136+
- Too globally invasive ❌
137+
138+
**11. Reconstructing parameters using `_zod.constr`**
139+
- `parameters._zod.constr` is the ZodObject constructor from host's zod
140+
- Can create new schema instances via `new parameters._zod.constr(def)`
141+
- Doesn't help: recreating schemas is complex, and we'd still need host's `describe()` context
142+
143+
**12. `meta()` method**
144+
- `schema.meta()` (no args) → `core.globalRegistry.get(schema)` — returns metadata object or undefined
145+
- `schema.meta(obj)``core.globalRegistry.add(clone, obj); return clone` — same as describe() pattern
146+
- Cannot extract the registry from either form
147+
148+
**13. `parameters.meta()` as registry sentinel**
149+
- After `parameters.describe("probe")`, the clone is IN host's registry
150+
- `clone.description === "probe"` confirms host registry works
151+
- Still no way to get a reference to the registry from the clone
152+
153+
### How Descriptions Are Actually Stored (Zod v4 internals)
154+
155+
- `describe(desc)`: `const cl = inst.clone(); core.globalRegistry.add(cl, { description: desc }); return cl`
156+
- `description` getter: `core.globalRegistry.get(inst)?.description`
157+
- `$ZodRegistry.get(schema)`: checks `schema._zod.parent` for inheritance, then `_map.get(schema)`
158+
- `JSONSchemaGenerator._metadataRegistry`: set from `params?.metadata ?? registries_js_1.globalRegistry`
159+
- During schema emit: `const meta = this.metadataRegistry.get(schema); if (meta) Object.assign(result.schema, meta)`
160+
- Descriptions (and all other metadata) are stored in `$ZodRegistry._map` (a `WeakMap<schema, meta>`)
161+
162+
### Final Working Solution
163+
164+
Dynamic `import('zod')` in the `tool.definition` hook + `globalRegistry.add()` for each field schema:
165+
- Works because in production (peerDep, no local copy) the import resolves to host's zod module cache
166+
- `output.parameters._zod.def.shape` gives access to the original plugin field schemas
167+
- `fieldSchema.description` reads from plugin's registry cross-instance (it's just a getter calling `core.globalRegistry.get(inst)`)
168+
- `hostRegistry.add(fieldSchema, { description })` makes the SAME schema object findable in host's registry
169+
- All 64 tests pass
170+
86171
## Implementation Plan (Code Phase Tasks)
87172

88173
1. **`opencode-plugin/package.json`**: Move `zod` from `dependencies` to `peerDependencies` with version `">=4.1.8"`

0 commit comments

Comments
 (0)