689 lines
34 KiB
Markdown
689 lines
34 KiB
Markdown
# 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":"<form-file-name>"}}
|
||
```
|
||
|
||
```json
|
||
{"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:
|
||
|
||
```json
|
||
{"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.
|
||
|
||
```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
|
||
`ОбщаяФорма.<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:
|
||
|
||
```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
|
||
`ОбщаяФорма.<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:
|
||
|
||
```json
|
||
{"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:
|
||
|
||
```json
|
||
{"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:
|
||
|
||
```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
|
||
```
|
||
|
||
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:
|
||
|
||
```text
|
||
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:
|
||
|
||
```text
|
||
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:
|
||
|
||
```text
|
||
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:
|
||
|
||
```text
|
||
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 `ConfigCASSave` rows:
|
||
`<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`;
|
||
- `ConfigSave` remained empty for these cases.
|
||
|
||
Direct saved-state module reads should use a concrete stream when known:
|
||
|
||
```json
|
||
{"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:
|
||
|
||
```json
|
||
{"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:
|
||
|
||
```json
|
||
{"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:
|
||
|
||
```text
|
||
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:
|
||
|
||
```json
|
||
{"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 `old` and `new`;
|
||
- routine replacement/append/upsert: pass `routine_name`, `routine_operation`,
|
||
and `routine_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:
|
||
|
||
```json
|
||
{"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`:
|
||
|
||
```json
|
||
{"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:
|
||
|
||
```json
|
||
{"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:
|
||
|
||
```text
|
||
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.
|