Files
llm/docs/1c-write-handler-architecture.md
2026-08-14 09:40:51 +03:00

5.3 KiB

1C Adapter: universal write dispatcher and typed handlers

Decision

The public write contract stays universal and name-first:

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

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.