Initial SQL-only 1C adapter baseline
This commit is contained in:
@@ -0,0 +1,688 @@
|
||||
# 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.
|
||||
Reference in New Issue
Block a user