|
| 1 | +# V2 API Dynamic Impact |
| 2 | + |
| 3 | +Date: 2026-03-01 |
| 4 | +Scope: `packages/schema2` + `packages/dynamic` + runtime dependents |
| 5 | + |
| 6 | +## Goal |
| 7 | + |
| 8 | +Define a realistic path to either: |
| 9 | + |
| 10 | +1. merge `packages/dynamic` into the new schema design, or |
| 11 | +2. keep `packages/dynamic` but reduce it to a thin, focused compatibility layer. |
| 12 | + |
| 13 | +The target is a radically simpler, coherent API with clear ownership boundaries. |
| 14 | + |
| 15 | +## Current State (evidence) |
| 16 | + |
| 17 | +### Size snapshot |
| 18 | + |
| 19 | +- `packages/dynamic/src`: 18 PHP files, ~1137 LOC |
| 20 | +- `packages/schema2/src`: 30 PHP files, ~2772 LOC |
| 21 | + |
| 22 | +### Coupling snapshot |
| 23 | + |
| 24 | +`dynamic` currently depends on schema internals and concrete implementations: |
| 25 | + |
| 26 | +- `Cognesy\Schema\Reflection\ClassInfo` |
| 27 | +- `Cognesy\Schema\Reflection\FunctionInfo` |
| 28 | +- `Cognesy\Schema\Visitors\SchemaToJsonSchema` |
| 29 | +- `Cognesy\Schema\Factories\SchemaFactory` |
| 30 | +- `Cognesy\Schema\Factories\JsonSchemaToSchema` |
| 31 | +- `Cognesy\Schema\Data\TypeDetails` / `Data\Schema\*` |
| 32 | + |
| 33 | +`dynamic` is also used in runtime paths: |
| 34 | + |
| 35 | +- `instructor` (`ResponseModelFactory`) |
| 36 | +- `agents` (ReAct tool-call normalization paths, reflective schemas) |
| 37 | +- `addons` (`FunctionCall`, ToolUse ReAct paths) |
| 38 | +- `experimental` (`Signature`, RLM protocol structures) |
| 39 | + |
| 40 | +## Design Problem |
| 41 | + |
| 42 | +`dynamic` currently mixes: |
| 43 | + |
| 44 | +- schema definition |
| 45 | +- schema reflection |
| 46 | +- runtime value container |
| 47 | +- validation |
| 48 | +- transformation |
| 49 | +- serialization |
| 50 | + |
| 51 | +This causes ambiguous ownership between `schema2` and `dynamic`, and keeps both packages larger than needed. |
| 52 | + |
| 53 | +## Option A: Merge Dynamic into Schema2 |
| 54 | + |
| 55 | +## What this means |
| 56 | + |
| 57 | +Move dynamic capabilities under `Cognesy\Schema` as schema-adjacent runtime modules and remove `packages/dynamic` as a separate domain package. |
| 58 | + |
| 59 | +Suggested internal split: |
| 60 | + |
| 61 | +- `Schema\Model\*` (existing schema nodes / type metadata) |
| 62 | +- `Schema\Runtime\Record\*` (array-backed record container) |
| 63 | +- `Schema\Runtime\Normalize\*` |
| 64 | +- `Schema\Runtime\Validate\*` |
| 65 | +- `Schema\Runtime\Hydrate\*` |
| 66 | +- `Schema\Runtime\Legacy\*` (temporary `Structure`/`Field` adapter layer) |
| 67 | + |
| 68 | +## Pros |
| 69 | + |
| 70 | +- one package boundary for schema + schema-driven record behavior |
| 71 | +- easier removal of duplicate reflection/type logic |
| 72 | +- strongest path to large LOC reduction by deleting bridging layers |
| 73 | + |
| 74 | +## Cons |
| 75 | + |
| 76 | +- highest migration blast radius (autoload + package identity + callsites) |
| 77 | +- higher short-term regression risk in `instructor/agents/addons/experimental` |
| 78 | +- harder rollback if merge and redesign happen simultaneously |
| 79 | + |
| 80 | +## When to choose |
| 81 | + |
| 82 | +Choose Option A only if: |
| 83 | + |
| 84 | +- we accept larger one-time migration risk in exchange for fastest simplification |
| 85 | +- we can commit to aggressive cross-package callsite updates in the same window |
| 86 | + |
| 87 | +## Option B: Keep Dynamic, Drastically Simplify It (recommended first step) |
| 88 | + |
| 89 | +## What this means |
| 90 | + |
| 91 | +Keep `packages/dynamic`, but make it a thin compatibility facade over schema2 contracts and array-first runtime processing. |
| 92 | + |
| 93 | +Target design for `dynamic`: |
| 94 | + |
| 95 | +- keep only public compatibility surface needed by runtime callsites |
| 96 | +- remove direct imports of `Schema\Reflection\*`, `Schema\Utils\*`, concrete visitors |
| 97 | +- represent runtime data as associative arrays, not mutable field graphs |
| 98 | +- keep `Structure` API as deprecated adapter during transition |
| 99 | + |
| 100 | +## What should remain public in dynamic |
| 101 | + |
| 102 | +- `StructureFactory` (compat entrypoint, internally delegated) |
| 103 | +- `Structure` (compat wrapper, deprecated) |
| 104 | +- minimal adapter helpers required by existing callsites |
| 105 | + |
| 106 | +## What should be removed or internalized in dynamic |
| 107 | + |
| 108 | +- `Field` as primary runtime model (replace with record/map backing) |
| 109 | +- trait-heavy mutable internals that duplicate schema/runtime concerns |
| 110 | +- schema reflection logic duplicated in dynamic |
| 111 | + |
| 112 | +## Pros |
| 113 | + |
| 114 | +- lower migration risk than full merge |
| 115 | +- clear path to remove schema internal dependencies immediately |
| 116 | +- rollback-friendly and incremental |
| 117 | + |
| 118 | +## Cons |
| 119 | + |
| 120 | +- temporary two-package setup remains during migration |
| 121 | +- requires discipline to avoid new logic entering dynamic |
| 122 | + |
| 123 | +## When to choose |
| 124 | + |
| 125 | +Choose Option B when: |
| 126 | + |
| 127 | +- we need fast risk-managed progress to 2.0 |
| 128 | +- we want measurable reduction before deciding on final merge |
| 129 | + |
| 130 | +## Recommended Strategy |
| 131 | + |
| 132 | +Use Option B now, keep Option A as Phase-2 consolidation decision. |
| 133 | + |
| 134 | +Reason: |
| 135 | + |
| 136 | +- fastest path to enforce clean boundaries |
| 137 | +- smallest regression envelope for runtime packages |
| 138 | +- preserves optional later merge once compatibility pressure is reduced |
| 139 | + |
| 140 | +## Implementation Plan |
| 141 | + |
| 142 | +### Phase 1: Boundary hardening |
| 143 | + |
| 144 | +- ban new non-schema imports of `Cognesy\Schema\Reflection\*` from dynamic |
| 145 | +- replace dynamic reflection usage with native reflection + TypeInfo in dynamic-local adapters |
| 146 | +- replace direct `SchemaToJsonSchema` usage with schema rendering contract |
| 147 | + |
| 148 | +Acceptance: |
| 149 | + |
| 150 | +- no `use Cognesy\Schema\Reflection\*` in `packages/dynamic/src` |
| 151 | +- no `use Cognesy\Schema\Utils\*` in `packages/dynamic/src` |
| 152 | + |
| 153 | +### Phase 2: Runtime model simplification |
| 154 | + |
| 155 | +- introduce array-backed record representation in dynamic |
| 156 | +- route normalization/validation through modular processors |
| 157 | +- keep `Structure` methods as compatibility wrappers over record processors |
| 158 | + |
| 159 | +Acceptance: |
| 160 | + |
| 161 | +- core runtime flows no longer rely on mutable per-field object state |
| 162 | +- `Structure` remains functional but delegates internally |
| 163 | + |
| 164 | +### Phase 3: Downstream migration |
| 165 | + |
| 166 | +- `instructor`: JSON-schema fallback path uses record pipeline, not legacy structure mutation |
| 167 | +- `agents` / `addons`: ReAct arg normalization uses schema+record processors |
| 168 | +- `experimental`: move signature-specific metadata helpers out of schema internals |
| 169 | + |
| 170 | +Acceptance: |
| 171 | + |
| 172 | +- runtime packages stop depending on dynamic internals beyond compatibility API |
| 173 | + |
| 174 | +### Phase 4: Consolidation decision |
| 175 | + |
| 176 | +Evaluate: |
| 177 | + |
| 178 | +- remaining dynamic LOC |
| 179 | +- remaining dynamic runtime ownership |
| 180 | +- regression and maintenance cost |
| 181 | + |
| 182 | +Decision: |
| 183 | + |
| 184 | +- if dynamic reduced to thin wrapper only -> either keep as compatibility package or merge into schema with minimal risk |
| 185 | +- if meaningful unique domain remains -> keep as separate package with strict scope |
| 186 | + |
| 187 | +## Merge Readiness Criteria (if we choose Option A later) |
| 188 | + |
| 189 | +Before merge: |
| 190 | + |
| 191 | +- dynamic contains no schema-internal reflection dependencies |
| 192 | +- dynamic data model is array-first and modularized |
| 193 | +- downstream callsites consume stable schema/runtime contracts |
| 194 | + |
| 195 | +Only then merge package boundaries. Do not merge while internals are still entangled. |
| 196 | + |
| 197 | +## Success Metrics |
| 198 | + |
| 199 | +Track after each phase: |
| 200 | + |
| 201 | +- LOC delta (`dynamic`, `schema2`, and combined total) |
| 202 | +- import bans compliance |
| 203 | +- monorepo `composer test` pass |
| 204 | +- impacted examples pass (`instructor`, `agents`, `addons`, `experimental`) |
| 205 | + |
| 206 | +## Decision Summary |
| 207 | + |
| 208 | +- Immediate path: keep `dynamic` package, simplify aggressively, enforce clean schema boundary. |
| 209 | +- Deferred path: merge into schema only after simplification removes coupling and shrinks compatibility surface. |
| 210 | + |
| 211 | +This sequencing gives the best chance of radical simplification with controlled delivery risk. |
0 commit comments