# 1C Adapter: universal write dispatcher and typed handlers ## Decision The public write contract stays universal and name-first: ```text code.write / metadata.write -> resolve public object and extension layer -> plan and gates -> select one typed handler -> prepare saved state internally when required -> apply, SQL-readback, rollback evidence ``` The caller never selects an SQL table, saved file, stream, payload codec, or handler. If no handler has a proven capability for the requested object type and operation, the dispatcher returns `unsupported_write_target` with a public explanation. It must not fall back to a generic byte rewrite. ## Current state The behaviour is already logically separated, but is physically concentrated in `plugins/1c/connector/adapter_1c_server.py` (about 3.5 MB). The main dispatcher is `metadata_write` and currently branches to: | Public target | Existing internal writer | |---|---| | BSL module | `metadata_module_write_apply` | | BSL embedded in a managed form | `form_embedded_module_handler_write_apply` | | Form element/property | `metadata_form_element_write_apply` | | Form command/button caption | `metadata_form_command_button_write` | | Scalar object/member property | `metadata_object_property_write` | | Add object member | `metadata_object_member_add` | | Scheduled-job schedule | `metadata_scheduled_job_schedule_write` | This is a suitable functional base. The problem is coupling: routing, saved-state preparation, result shaping, codecs, SQL writes, and HTTP/RPC dispatch live in one module, so a change in one type is too likely to affect another. ## Target module layout ```text plugins/1c/connector/ adapter_1c_server.py # HTTP, RPC registration, composition root only write/ contracts.py # WriteIntent, WritePlan, WriteResult, capability errors dispatcher.py # universal metadata.write dispatch; no SQL codecs gates.py # layer, repository, optimistic-hash and mode gates saved_state.py # Config→ConfigSave / ConfigCAS→ConfigCASSave prepare + receipt rollback registry.py # handler registration and deterministic selection handlers/ module.py embedded_form_module.py form_element.py form_command.py object_property.py object_member.py scheduled_job.py unsupported.py storage/ sql_saved_state.py # transactions, guarded row copy, backup/readback extension_routes.py # active-to-saved route and cache refresh ``` `parser/` remains the place for pure payload decoding/encoding. A handler may use a parser codec only where its round-trip proof exists; SQL access is provided through a narrow context rather than imported globals. ## Handler contract Each handler implements the same four operations: 1. `can_handle(intent, evidence) -> supported | unsupported | ambiguous`. 2. `plan(intent, evidence) -> WritePlan` with exact guards and no mutation. 3. `apply(plan, context) -> WriteResult` only after shared gates succeed. 4. `rollback(result, context)` when the handler created reversible state. The dispatcher selects exactly one handler. Zero handlers yields `unsupported_write_target`; multiple handlers yield `ambiguous_write_handler`. Handlers never select another extension layer after dispatch. `metadata.write.capabilities` also returns `registered_handlers`. This makes the runtime registry visible beside the broader capability matrix and prevents an API claim from silently drifting away from the installed handlers. ## Invariants owned centrally - public name/ref and extension scope resolve before handler selection; - only saved layers are writable; - saved-state preparation is internal and idempotent; - optimistic hash, audit event, backup/receipt, SQL readback and rollback policy are common infrastructure; - SQL readback is not described as Configurator activation; - low-level storage fields are redacted from name-first responses. ## Safe migration order 1. **Completed:** add storage-free `write/contracts.py`, typed handler declarations under `write/handlers/`, and `write/registry.py`; connect `metadata.write` to the registry while delegating to existing writers unchanged. The registry has a deny-by-default result for unknown target kinds. 2. **In progress:** add `write/context.py`; the scheduled-job route now enters its typed handler through this explicit context. The handler still delegates to the single existing implementation until its body moves in one change. 3. Move `saved_state.py` and `storage/sql_saved_state.py` first. The recently proven hash-keyed extension prepare/rollback smoke is its acceptance test. 4. Extract the least coupled handlers: scheduled job, object property, object member. 5. Extract form element and form command handlers. 6. Extract module and embedded-form-module handlers last; retain their exact payload codec and paired-write tests. 7. Reduce `metadata_write` to validation, plan/gate orchestration and one registry call. Keep the old public API names and response schema intact. Do not split by copying code into parallel paths. Each extraction must move one authoritative implementation, keep the existing tests green, and add one handler-level `plan → apply_and_rollback → readback` test in `upo_test` when a matching fixture exists.