# 1C Write Path Safety Status: active configuration writes are blocked by design. Saved-state form writes are allowed only through the explicit proposal/apply gates described below. Layer and addressing work plan: `docs/1c-extension-layer-plan.md`. The adapter may read SQL, Config, ConfigCAS, XML exports, and extension manifests. It must not write active SQL metadata/data tables or active Config payloads until the following gates are implemented and tested. The only current write exception is saved-state form payload editing in `ConfigSave` and `ConfigCASSave`. ## Required Gates 1. Backup gate Every write run must record database, configuration state, extension state, SQL transaction boundary, and rollback instructions before changing anything. 2. Round-trip parser gate The adapter must decode target payload, encode it back without semantic changes, and prove byte/structural equivalence where applicable. 3. Designer validation gate Any metadata write must be validated by 1C tools, not only by SQL shape. 4. Saved-state gate The adapter must distinguish active configuration from saved but not applied state, including `ConfigSave` and extension saved-state equivalents. 5. Extension packaging gate Extension changes must preserve manifest/CAS relationships and validate extension load/update behavior. 6. Diff gate Every proposed write must produce a human-readable diff at metadata level and physical storage level. 7. Minimal write scope gate Writes must target the smallest possible object or payload. Bulk Config rewrites are forbidden unless no narrower route exists. 8. Recovery test gate Restore/rollback must be tested on a disposable database before production write support is enabled. 9. Full-path target gate Every write intent must resolve to a full 1C path or a concrete saved-state reference before planning. Short names and local BSL symbols are acceptable only after they are bound to an object, form, module, routine, or query context. 10. Layer provenance gate Every write plan must identify whether the effective element comes from the base configuration, a selected extension, generated extension source, or a saved-state working copy. Effective text alone is not a writable target. 11. Extension control gate Extension code operations must classify insert-before, insert-after, replace, and replace-with-control separately. Replace-with-control writes must verify the controlled fragment against the current source before apply. ## Allowed Now - read metadata from SQL/XML/CAS; - build object read views; - resolve references; - inspect forms/modules/templates; - generate proposed code or metadata patches as files; - produce diffs and implementation plans; - plan saved-state form edits with `metadata.write` or `metadata.form.element.write`; - apply saved-state form proposals only when all are true: `allow_sql_saved_state_apply=true`, the proposal contains `validation.mode=path_preserve_format`, current sha1 matches, and backup evidence is written before update; - rollback saved-state form writes through `storage.saved_state.rollback`. - build read-only write plans that resolve full 1C paths, provenance, and required guards before a future apply route is selected. - use `metadata.write` as the normal agent-facing BSL write facade. It accepts canonical 1C paths, module/routine names, and code text, defaults to saving into `ConfigSave`/`ConfigCASSave`, and hides physical storage details unless `include_storage=true`. `code.write` remains a compatibility shortcut for simple module edits. ## Forbidden Now - direct updates to `_Reference*`, `_Document*`, register tables, `Config`, `ConfigCAS`, or active extension CAS tables; - saved-state writes that rewrite the whole serialized form payload instead of patching exact scalar token spans; - agent-facing code writes that require callers to know SQL tables, file names, stream indexes, or brace paths; - automatic Designer update/apply; - extension rebuild/writeback; - direct writes to effective views without layer provenance and write-plan evidence; - treating a local BSL expression such as `<Переменная>.<Поле>` as a metadata path without code-symbol resolution in the current context; - any write action that does not have rollback evidence. ## Saved-State Form Write Contract Saved-state form writes must: - resolve the display source before editing. If an element caption is empty and inherited from a linked command, write the command caption, not the empty element caption; - use the form property registry for aliases and verification behavior instead of ad-hoc property matching; - return registry metadata for decoded parameter properties, including stable canonical names such as `visible`, `enabled`, and `command_bar_location`; - avoid silently writing a local element title when the displayed caption is derived from `ПутьКДанным`; local title override must be explicit with `source=local_override`; - when an empty element title is derived through `ПутьКДанным`, route by source: form attribute -> write the form attribute title; tabular form attribute field such as `ТЗ.К1` -> write the field title; object/configuration attribute (`Объект.<Реквизит>`) -> write the local form element title; - patch a single scalar brace token in the original decoded text and preserve surrounding formatting/compression envelope; - return a semantic diff and physical sha1/byte metadata before apply; - create rollback evidence under `ONEC_ADAPTER_BACKUP_DIR` or `/data/adapter-apply-backups`; - verify readback sha1 and re-decode the form for semantic verification. ## Saved-State BSL Write Contract Agents edit modules as text, at the same level as a human programmer: - read current code from the working/save state; - replace the whole module with `module_text`, `full_text`, or `code`; - replace one procedure/function with `routine_name` and `routine_text`; - replace a smaller fragment with `old` and `new`. If `routine_name` or a routine-level `canonical_path` is provided, uniqueness is checked inside that procedure/function; otherwise `old` must occur exactly once in the current saved module text. The adapter maps that request to the physical saved-state route. For ordinary module streams it uses the saved-state module writer. For embedded form modules it patches only the module scalar token in the form payload and verifies that the encoded payload changed no more than that token. If a fragment repeats in the selected scope, the adapter must return `ambiguous_fragment` with `scope` and `counts.occurrences` instead of choosing an occurrence. ## Saved-State Preparation Gate An empty `ConfigSave` or `ConfigCASSave` means there are no unactivated Configurator changes in that save layer. To test or apply saved-state writes for an object, first prepare a working copy from the active storage family: `Config -> ConfigSave` for base configuration objects, or `ConfigCAS -> ConfigCASSave` for extension/CAS objects. The preparation flow is intentionally split into reviewable stages: ```text python scripts/plan_1c_saved_state_copy.py --base-id upo_test --target-table ConfigSave --report reports/1c-sql/upo_test/saved-state-copy-plan.json --json python scripts/prepare_1c_saved_state_copy_sql.py --plan reports/1c-sql/upo_test/saved-state-copy-plan.json --expected-base-id upo_test --expected-target-table ConfigSave --sql-out reports/1c-sql/upo_test/prepare-saved-state-copy.sql --report reports/1c-sql/upo_test/prepare-saved-state-copy-sql.json --json powershell -NoProfile -ExecutionPolicy Bypass -File scripts\execute_1c_saved_state_copy_sql.ps1 -ExpectedBaseId upo_test -ExpectedTargetTable ConfigSave -IUnderstandThisWritesToSql python scripts/verify_1c_saved_state_copy.py --plan reports/1c-sql/upo_test/saved-state-copy-plan.json --expected-base-id upo_test --expected-target-table ConfigSave --require-ready --json ``` Only `execute_1c_saved_state_copy_sql.ps1` performs SQL writes, and it requires the explicit `-IUnderstandThisWritesToSql` gate. Normal adapter verification generates the plan and reviewed SQL artifacts but does not execute them. ## Saved-State Write Route Smoke After changing saved-state form write routing or redeploying the REST adapter, run: ```text python scripts/smoke_1c_saved_state_write_routes.py \ --report reports/1c-sql/upo_test/saved-state-write-routes-smoke.json ``` The smoke test uses only the REST adapter URL and performs `apply_and_rollback` against `upo_test`. It must verify these route families: - form element with `ПутьКДанным = А` writes the form attribute title; - form element with `ПутьКДанным = ТЗ.К1` writes the tabular form attribute field title; - `command=КомандаПример1` writes the command title directly. ## Saved-State Write Matrix To automatically enumerate decoded scalar values and learn which ones can be changed safely, use the write matrix methods: ```text python scripts/smoke_1c_write_matrix.py \ --max-candidates 50 \ --report reports/1c-sql/upo_test/write-matrix-smoke-50.json ``` For a full verified registry on the current `upo_test` learning form: ```text python scripts/smoke_1c_write_matrix.py \ --max-candidates 1000 \ --report reports/1c-sql/upo_test/write-matrix-smoke-full.json python scripts/build_1c_write_matrix_verified_registry.py \ --smoke-report reports/1c-sql/upo_test/write-matrix-smoke-full.json \ --output reports/1c-sql/upo_test/write-matrix-verified-registry.json \ --include-route-evidence python scripts/build_1c_write_matrix_enum_registry.py \ --matrix-report reports/1c-sql/upo_test/write-matrix-build-v4.json \ --output reports/1c-sql/upo_test/write-matrix-enum-registry.json python scripts/build_1c_write_matrix_scalar_registry.py \ --matrix-report reports/1c-sql/upo_test/write-matrix-build-v4.json \ --output reports/1c-sql/upo_test/write-matrix-scalar-registry.json python scripts/build_1c_write_learning_plan.py \ --registry reports/1c-sql/upo_test/write-matrix-scalar-registry.json \ --output reports/1c-sql/upo_test/write-learning-plan-scalar.json python scripts/analyze_1c_write_matrix_structural_diff.py \ --before reports/1c-sql/upo_test/write-matrix-build-v4.json \ --after reports/1c-sql/upo_test/write-matrix-build-after-move-a.json \ --output reports/1c-sql/upo_test/write-matrix-structural-diff-move-a-after-b.json ``` ```json {"method":"metadata.form.write_matrix.build","payload":{"base_id":"upo_test","table":"ConfigCASSave","file_name":""}} ``` ```json {"method":"metadata.form.write_matrix.smoke","payload":{"base_id":"upo_test","table":"ConfigCASSave","file_name":"","allow_sql_saved_state_apply":true,"allow_sql_saved_state_rollback":true,"max_candidates":10,"learning_id":"upo-test-write-matrix"}} ``` `build` returns every decoded writable scalar candidate with `can_smoke` and a reason when it is not safe for generic smoke. `smoke` runs only `can_smoke` entries through `apply_and_rollback`, records verification status, and stores a report under `ONEC_ADAPTER_WRITE_LEARNING_DIR` when `learning_id` is passed. Matrix entries include `semantic_name`, `semantic_group`, and `semantic_source` when the decoded form semantic map knows the parameter, so gap analysis can group unresolved properties by configurator-facing meaning instead of only by physical `Параметр N`. For empty local strings, `build` also adds `codec_probe` diagnostics. Direct empty `string` nodes are smoke-safe; composite empty string/list nodes remain classified as `composite_node_requires_semantic_rule` after probing because they usually represent typed structures such as layout, decoration, references, or containers rather than plain strings. Current generic smoke intentionally skips identity/binding values (`id`, `name`, `ПутьКДанным`), enum/color values without known allowed sets, and composite nodes that need a source-specific semantic write rule. The scalar registry separates these skipped scalar values into learning queues: `run_before_after_learning_for_parameter` is safe to learn with manual Configurator before/after captures, `learn_reference_write_rule` needs a source-specific reference/container rule, and `do_not_generic_write` remains manual-only because it covers identity or structural values. The learning plan turns the scalar or enum registry into an ordered queue of manual learning cases. Each case contains a selector, current value, physical write path, and the workflow: `capture_before -> manual_configurator_change -> capture_after -> diff -> infer_rule -> smoke_rule`. Moving form elements is a structural operation, not a scalar property write. `metadata.write_learning.diff` reports these as `target_moves` by stable target identity (`section`, `name`, `id`) when a target path changes. Property inference then returns `structural_move_rule_required` until a dedicated saved-state move/reorder writer is implemented and smoke-tested. The first structural writer is `metadata.form.target.move`. It currently supports the learned safe primitive `swap_sibling_slots`: two form item nodes in the same parent container are swapped by byte-preserving brace-text ranges (`swap_paths`) and then passed through the same saved-state proposal, backup/apply, and rollback gates as scalar writes. Example: ```json {"method":"metadata.form.target.move","payload":{"base_id":"upo_test","table":"ConfigCASSave","file_name":"","from_element":"А","to_element":"Б","allow_saved_state_write":true,"mode":"apply_and_rollback","allow_sql_saved_state_apply":true,"allow_sql_saved_state_rollback":true}} ``` The second structural primitive is `append_child`, used by `metadata.form.command_button.write` to add a form command and a visible command bar button. The method clones existing command/button nodes from the same form, replaces name/title/action/GUID fields, appends the new nodes with byte-preserving brace-text insertion, updates declared section counts such as `{marker,count,record...}` when the append target uses them, then uses the same proposal, backup, apply, verify, and rollback gates. If the save layer is empty, `plan` exposes `metadata.saved_state.prepare`; apply modes with `allow_sql_saved_state_apply` prepare saved state internally before retrying. After `apply`/`apply_and_verify`, the method re-decodes the saved-state form and returns `semantic_verify`: command present, button present, handler routine present, command->handler link, button->command link, and the decoded command/button paths. Repeated `upsert` calls are idempotent: if command and button already exist, the method reports `idempotency.status=already_exists`, updates/verifies the handler, and does not append duplicate structural nodes. Successful saved-state module changes refresh `metadata_code_index_cache` and vector chunks for the embedded form module, while SQL remains the freshness source of truth. Use `metadata.form.command_button.verify` when no write is needed. It accepts the same public selectors (`extension`, `form`, `command_name`, `button_name`, `handler_name`) and returns the same command/button/handler/link checks from current SQL saved-state. Public command/button write and verify responses keep saved-state search compact: `counts` plus `selected_form`, not the full list of similar form candidates. Public form node addresses are exposed as `form_path`; physical SQL `file_name` remains opt-in through `include_storage=true`. By default the same method also upserts the form-module handler routine named by `command_action`. For SQL form payloads where the module is embedded as a scalar BSL string, the writer edits module path `2` with `replace_routine_text` and a byte-preserving scalar proposal. Pass `include_handler=false` only for diagnostics that intentionally validate the structural form append alone. ```json {"method":"metadata.form.command_button.write","payload":{"base_id":"upo_test","extension":"test2","object_type":"CommonForm","object_name":"t_Форма","command_name":"РасчетС","command_title":"РасчетС","command_action":"РасчетС","allow_saved_state_write":true,"mode":"plan"}} ``` For ordinary agent work, prefer the high-level `metadata.write` entrypoint when only the form module routine must be changed. It resolves `ОбщаяФорма.
.` to the saved-state form payload, treats missing routine names in `upsert` mode as an owner-only module search, edits embedded module path `2`, and keeps the save-first defaults on apply modes: ```json {"method":"metadata.write","payload":{"base_id":"upo_test","target":{"canonical_path":"ОбщаяФорма.t_Форма.РасчетС"},"mode":"apply_and_rollback","routine_operation":"upsert","routine_text":"&НаКлиенте\nПроцедура РасчетС(Команда)\n\t// ...\nКонецПроцедуры\n","allow_sql_saved_state_rollback":true}} ``` For form commands/buttons, `metadata.write` can route explicit paths such as `ОбщаяФорма..Команда.` or `ОбщаяФорма..Кнопка.