# 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.
