34 KiB
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
-
Backup gate
Every write run must record database, configuration state, extension state, SQL transaction boundary, and rollback instructions before changing anything.
-
Round-trip parser gate
The adapter must decode target payload, encode it back without semantic changes, and prove byte/structural equivalence where applicable.
-
Designer validation gate
Any metadata write must be validated by 1C tools, not only by SQL shape.
-
Saved-state gate
The adapter must distinguish active configuration from saved but not applied state, including
ConfigSaveand extension saved-state equivalents. -
Extension packaging gate
Extension changes must preserve manifest/CAS relationships and validate extension load/update behavior.
-
Diff gate
Every proposed write must produce a human-readable diff at metadata level and physical storage level.
-
Minimal write scope gate
Writes must target the smallest possible object or payload. Bulk Config rewrites are forbidden unless no narrower route exists.
-
Recovery test gate
Restore/rollback must be tested on a disposable database before production write support is enabled.
-
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.
-
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.
- 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.writeormetadata.form.element.write; - apply saved-state form proposals only when all are true:
allow_sql_saved_state_apply=true, the proposal containsvalidation.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.writeas the normal agent-facing BSL write facade. It accepts canonical 1C paths, module/routine names, and code text, defaults to saving intoConfigSave/ConfigCASSave, and hides physical storage details unlessinclude_storage=true.code.writeremains 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, andcommand_bar_location; - avoid silently writing a local element title when the displayed caption is
derived from
ПутьКДанным; local title override must be explicit withsource=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_DIRor/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, orcode; - replace one procedure/function with
routine_nameandroutine_text; - replace a smaller fragment with
oldandnew. Ifroutine_nameor a routine-levelcanonical_pathis provided, uniqueness is checked inside that procedure/function; otherwiseoldmust 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:
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:
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
ПутьКДанным = ТЗ.К1writes the tabular form attribute field title; command=КомандаПример1writes 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:
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:
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
{"method":"metadata.form.write_matrix.build","payload":{"base_id":"upo_test","table":"ConfigCASSave","file_name":"<form-file-name>"}}
{"method":"metadata.form.write_matrix.smoke","payload":{"base_id":"upo_test","table":"ConfigCASSave","file_name":"<form-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:
{"method":"metadata.form.target.move","payload":{"base_id":"upo_test","table":"ConfigCASSave","file_name":"<form-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.
{"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
ОбщаяФорма.<Form>.<Routine> 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:
{"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
ОбщаяФорма.<Form>.Команда.<Command> or
ОбщаяФорма.<Form>.Кнопка.<Button> to
metadata.form.command_button.write. For existing form properties, use
metadata.form.write_target.verify first; it reports whether the saved-state
target is writable now or whether the adapter would need to prepare Save.
Every write call made through the public adapter entrypoint, including
code.write, receives an operation_id. Use metadata.write.history to
retrieve recent operations, backup ids, routed method, and verification result.
History accepts operation_method, status, routed_method, and backup_id
filters for quick audit lookup. Pass include_summary=true to get aggregate
counts by method, status, routed method, and operations with backups.
Use metadata.write.rollback to rollback by operation_id or backup_id;
it still requires the explicit allow_sql_saved_state_rollback=true gate.
Deployment verification runs scripts/smoke_1c_write_rollback_safety.py for
REST and MCP. The smoke does not apply rollback; it checks that
metadata.write.rollback is exposed, metadata.write.history is readable, and
rollback without the explicit gate returns invalid_argument for
allow_sql_saved_state_rollback.
To inspect what is currently pending in Save, use metadata.saved_state.diff.
It is read-only and compares the saved payload with its active SQL source:
{"method":"metadata.saved_state.diff","payload":{"base_id":"upo_test","table":"ConfigCASSave","file_name":"<saved-state-file-name>","max_text_diff_lines":80}}
If the saved payload is missing, the result is status=not_found with
needs_prepare=true and a metadata.saved_state.prepare plan payload. If both
layers exist, the result is changed or unchanged with live SQL hashes.
Deployment verification runs scripts/smoke_1c_saved_state_diff.py for REST
and MCP and persists saved-state-diff-smoke.json plus
saved-state-diff-mcp-smoke.json.
For a save-layer overview, use metadata.saved_state.status before drilling
into a single file:
{"method":"metadata.saved_state.status","payload":{"base_id":"upo_test","table":"ConfigCASSave","limit":500}}
It returns row/file counts and classifies files as changed, unchanged, or
saved_only. Each file includes a diff_selector for metadata.saved_state.diff.
For readback by public name, metadata.form.decode honors
source_state=working/state=save: it searches ConfigCASSave/ConfigSave
first and uses extension to narrow extension forms before falling back to
active metadata routes.
Saved-State Copy Learning
An empty ConfigSave/ConfigCASSave means there is no pending Configurator
working copy. It does not mean the object is missing; the active object remains
in Config/ConfigCAS. To change an object through the saved-state write
path, first copy that same target object from the main configuration storage
into the save layer (ConfigSave/ConfigCASSave), then apply changes to the
save object and keep rollback evidence. To learn or verify the copy shape
safely, take a save-layer snapshot, create one minimal pending Configurator
change without applying it, then take another snapshot and compare the created
rows.
Before preparing a strict write smoke, generate a read-only copy plan:
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
The plan must be plan_ready, list active source rows from the matching storage
family, and report target_collisions.status=clear: Config -> ConfigSave for
base objects, ConfigCAS -> ConfigCASSave for extension/CAS objects. The
persisted report validator checks this plan by default, so a stale, colliding,
or family-mismatched save-layer target fails before any strict write smoke is
enabled.
After reviewing the copy plan, generate a guarded SQL preparation script without executing it:
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
The generated script repeats the collision check inside a transaction, copies
only the reviewed FileName values, verifies the inserted row count, and
commits only if those guards pass.
After the SQL preparation is executed, verify that the save layer contains the reviewed bytes:
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 \
--report reports/1c-sql/upo_test/saved-state-copy-verify.json \
--require-ready
Before executing the SQL preparation this should fail with
blocked_missing_target_rows; after preparation it must pass before strict
readiness or write-and-rollback smoke is enabled.
Generate the guarded cleanup script before executing the preparation SQL:
python scripts/prepare_1c_saved_state_cleanup_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/cleanup-saved-state-copy.sql \
--report reports/1c-sql/upo_test/cleanup-saved-state-copy-sql.json
The cleanup script deletes only the reviewed FileName/PartNo rows from
ConfigSave, checks their BinarySHA1 first, and rolls back if the save-layer
rows no longer match the plan.
Check strict readiness against the same target table:
python scripts/check_1c_saved_state_strict_readiness.py \
--base-id upo_test \
--saved-state-table ConfigSave \
--report reports/1c-sql/upo_test/saved-state-strict-readiness.json
Observed on upo_test:
- a saved form edit created three
ConfigCASSaverows:<object_guid>__<form_guid>,<object_guid>__<form_guid>.0, and<object_guid>__configinfo; - a saved object-module edit added two module rows:
<object_guid>__<module_guid>and<object_guid>__<module_guid>.0, and updated<object_guid>__configinfo; ConfigSaveremained empty for these cases.
Direct saved-state module reads should use a concrete stream when known:
{"method":"modules.read","payload":{"base_id":"upo_test","module_ref":"ConfigCASSave:<object_guid>__<module_guid>.0#stream:4","preview":true}}
For direct ConfigSave/ConfigCASSave module refs the adapter uses a synthetic
saved-state owner context and skips extension owner scans.
Do not pass a saved-state form payload container such as
ConfigCASSave:<extension_guid>__<form_guid>.0 to
metadata.module.write_apply as if it were a concrete BSL stream. That ref is
readable for embedded form module analysis, but stream writes require
#stream:<index> or an explicit stream_index. If the ref has no stream index,
metadata.write.plan must return ready_for_apply_method=false for the stream
writer and include next_resolution.
The high-level metadata.write entrypoint may still accept such a saved-state
form payload container when the request identifies one routine with
routine_name and routine_text. In that case it must route to the embedded
form payload writer (form_embedded_module_handler_write_apply), edit the BSL
text at the decoded form module path, and preserve the payload format instead
of rewriting the serialized form payload as plain module text. For routine
replacement in form modules, existing leading BSL directives such as
&НаКлиенте are preserved when the replacement routine text omits a directive.
When the concrete module stream is not known, search saved-state modules first:
{"method":"metadata.saved_state.modules.search","payload":{"base_id":"upo_test","tables":["ConfigCASSave"],"owner_guid":"<object_guid>","query":"<text-fragment>","limit":10}}
The search also accepts public object selectors. When
object_type/object_name resolve to one object, the adapter narrows the
saved-state scan by that owner GUID:
{"method":"metadata.saved_state.modules.search","payload":{"base_id":"upo_test","tables":["ConfigCASSave"],"object_type":"Catalog","object_name":"Номенклатура","query":"ПередЗаписью","limit":10}}
The search result returns streams[].module_ref, streams[].write_plan_target,
payload sha1, stream encoding, and a short preview. Use write_plan_target as
the concrete module target for metadata.write.plan, then use the accepted plan
with metadata.module.write_apply or the generic metadata.write module
target.
For saved-state form payloads, the search also detects the embedded form module
stored inside the decoded form payload. These rows return
payload.role=form_embedded_module_payload, streams[].module_path, and a
streams[].write_plan_target.module_ref without #stream. That target is for
the generic metadata.write embedded-form route, not for
metadata.module.write_apply. When object_type=CommonForm and
object_name are supplied, module search first resolves the exact saved-state
form file through metadata.saved_state.forms.search, so similarly named forms
such as t_Форма and tt_Форма3 do not broaden the module scan.
The generic metadata.write module target can use that selector-only path
directly. A form routine replacement may pass target.kind=module,
target.object_type=CommonForm, target.object_name, routine_name, and
routine_text without a module_ref; the adapter resolves the saved form
file, detects form_embedded_module_payload, and routes to the embedded-form
writer.
For common forms, the same path is available from a canonical path such as
ОбщаяФорма.t_Форма.ЗаменаДомена. The planner classifies it as a module
routine on a common form (path_kind=module_routine,
section=form_module), and metadata.write may resolve it through
metadata.saved_state.modules.search before routing to the embedded-form
writer.
Form module payload writes must never canonicalize the whole form payload. A
prior unsafe write showed that replacing path 2 with a normal structural
path edit can make Designer reject the saved form even when the adapter can
decode it. Embedded form module apply is therefore guarded in two places:
form_embedded_module_handler_write_apply verifies that the encoded payload is
exactly one byte-preserving scalar replacement, and
storage.saved_state.apply_proposal blocks form module path 2 edits unless
their edit mode is path_preserve_format.
Some saved form module scalars contain a trailing container marker such as
///----. This is not agent-facing BSL text. Public code.read and
code.search hide the trailing marker, while the embedded form module writer
preserves it automatically when an agent replaces the full module text.
Saved-state module writes use stream edits, not brace-path edits. Smoke the route with apply-and-rollback:
python scripts/smoke_1c_saved_state_module_write.py \
--file-name <object_guid>__<module_guid>.0 \
--stream-index 4 \
--expected-sha1 <current-payload-sha1> \
--report reports/1c-sql/upo_test/module-stream-write-smoke-script.json
The proposal must validate the BSL stream, update the stream header, apply with
a saved-state backup, verify readback sha1, and rollback to the original sha1.
If the disposable base currently has no pending Configurator saved-state rows,
pass --allow-empty-saved-state so the smoke records
skipped_no_saved_state instead of reporting a false route failure. For a real
write test, prepare the save layer by copying the module owner object from
Config/ConfigCAS into ConfigSave/ConfigCASSave first.
Agents should use the high-level method once the module saved-state row already exists:
{"method":"metadata.module.write_apply","payload":{"base_id":"upo_test","module_ref":"ConfigCASSave:<object_guid>__<module_guid>.0#stream:4","allow_saved_state_write":true,"mode":"apply_and_rollback","allow_sql_saved_state_apply":true,"allow_sql_saved_state_rollback":true,"expected_sha1":"<current-payload-sha1>","old":"<old-fragment>","new":"<new-fragment>","expected_contains":"<guard-fragment>"}}
Module text can be changed at three levels:
- full stream replacement: pass
text; - fragment replacement: pass
oldandnew; - routine replacement/append/upsert: pass
routine_name,routine_operation, androutine_text.
For full stream replacement, pass expected_text_sha1 from the current module
text in addition to expected_sha1 for the SQL payload. expected_text_sha1
uses normalized line endings, matching the code index freshness hash. This lets
agents verify that the BSL text they read is still the text being patched even
when SQL payload metadata or compression details differ.
This is a thin orchestrator over the proven stream writer: it normalizes
module_ref, builds changes.propose, applies through
storage.saved_state.apply_proposal, and can immediately rollback for smoke.
Before SQL apply it also runs metadata.write.plan; blocked plans, such as
replace_with_control without a control fragment, stop before the storage
apply call. If current-source evidence such as current_text or source_text
is supplied, replace_with_control also blocks with control_fragment_drift
when the controlled fragment no longer appears in that source.
The generic write entrypoint routes module targets to the same method:
{"method":"metadata.write","payload":{"base_id":"upo_test","target":{"kind":"module","module_ref":"ConfigCASSave:<object_guid>__<module_guid>.0#stream:4"},"mode":"apply_and_rollback","allow_sql_saved_state_apply":true,"allow_sql_saved_state_rollback":true,"expected_sha1":"<current-payload-sha1>","old":"<old-fragment>","new":"<new-fragment>","expected_contains":"<guard-fragment>"}}
If module_ref is omitted, metadata.write can resolve exactly one saved-state
module stream with metadata.saved_state.modules.search:
{"method":"metadata.write","payload":{"base_id":"upo_test","target":{"kind":"module","owner_guid":"<object_guid>"},"mode":"apply_and_rollback","allow_sql_saved_state_apply":true,"allow_sql_saved_state_rollback":true,"old":"<old-fragment>","new":"<new-fragment>","expected_contains":"<guard-fragment>"}}
Ambiguous or missing searches are reported as module_target_not_resolved; pass
module_ref, file_name, stream_index, or a narrower query.
For agents, saved-state preparation is an adapter concern, not a planning
choice. The agent asks to change the current module/form. The adapter reads the
active source from Config/ConfigCAS, creates or reuses the corresponding
working copy in ConfigSave/ConfigCASSave, writes only that saved-state copy,
and leaves activation to a human in 1C.
For the high-level metadata.write entrypoint, apply modes are save-first by
default. Agents should not ask users for SQL permission flags when the user asks
to save/edit metadata: call metadata.write with mode=apply and the adapter
sets allow_sql_saved_state_apply=true, allow_sql_saved_state_prepare=true,
and auto_prepare_saved_state=true internally. Low-level storage/apply tools
remain explicit, but the agent-facing writer has exactly one write destination:
ConfigSave/ConfigCASSave.
When metadata.write runs in plan mode and the save layer is empty, it returns
a metadata.saved_state.prepare plan. When metadata.write runs in an apply
mode, saved-state preparation is automatic by default. If a caller passes an active module ref such as
ConfigCAS:<file>#stream:<n>, the adapter prepares the matching saved-state ref
such as ConfigCASSave:<file>#stream:<n> and continues there. Direct writes to
Config/ConfigCAS remain blocked. Pass auto_prepare_saved_state=false only
for diagnostic tests that must prove the save layer is already present.
The same rule applies to form writes. If metadata.write receives a form target
in Config/ConfigCAS, it prepares the matching ConfigSave/ConfigCASSave
row and then routes the scalar form edit to
metadata.form.element.write_apply. If the saved-state form row is missing, a
plan response exposes metadata.saved_state.prepare; an apply mode with
metadata.write performs that preparation internally and retries against the
save table.
Routine-level module edits can be passed without manually building the low-level
routine object:
{"method":"metadata.write","payload":{"base_id":"upo_test","target":{"kind":"module","owner_guid":"<object_guid>"},"mode":"plan","routine_name":"<procedure-or-function>","routine_operation":"replace","routine_text":"<full procedure/function text>","expected_old_contains":"<guard-fragment>"}}
Supported routine_operation values are replace, append, and upsert.
The routine writer preserves the surrounding module bytes: it keeps the module
EOL style and replaces only the exact routine span, not the separators before or
after it. Routine expected_old_sha1 is calculated over that exact routine span.
Preferred First Write Path
The first production-safe write path should be extension-first:
generate extension source
-> validate in 1C tooling
-> package extension
-> load into disposable base
-> run smoke tests
-> produce human approval diff
Direct SQL metadata writes remain a research path until all gates above are green.