Initial project import
This commit is contained in:
@@ -0,0 +1,121 @@
|
||||
# 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.
|
||||
Reference in New Issue
Block a user