Structural batch editing¶
Waymark discovers exact structural identities. PatchDIFF observes disjoint scopes on one canonical target snapshot, evaluates each operation group inside its own original slice, and commits the assembled target once. The host owns selection, intent, proposal generation and retry decisions. A batch never spans targets.
The MCP profile catalog lists the connections that expose these tools.
Observation contract¶
Use the tools/list examples for request shapes. Replace illustrative IDs,
guards and cursors with values from actual receipts.
- List candidates if you need to find a structural identity. A candidate list carries no observation ID. You can select a known identity directly.
- Select one scope or a batch. Keep the exact names returned by discovery, including Markdown punctuation. Ambiguous identities are rejected.
- Page the observed text if needed. Submit against the observed scope or batch and keep their argument sets separate.
Exact match strings can include literal newlines. find_occurrences counts
matches across the current target. A batch group counts overlapping matches
from one within its own scope-local buffer after earlier operations in that
group.
External response presentation is independently selected at startup with
PATCHDIFF_MCP_RESPONSE_MODE=detailed|compact (default detailed). Input schemas,
guards, profiles, stored evidence and recovery semantics are unchanged. In compact
mode candidate entries retain name/kind/parent/start_line/end_line. Batch scopes
omit duplicate start/end/max_chars/next_start/start_line/end_line fields but retain
exact text, original bounds/hash, offset/next_offset, truncation and requested
absolute code-point line_starts. Single observations/pages and failures are
unchanged. Accepted observed submissions omit evidence/groups; counts, identities,
all guards/version aliases, validity, request key, deduplication and all existing
warning fields remain. A presentation object lists actual omissions. Detailed
sessions and durable logs/ledger retain full evidence. See
the response contract and
editor workflow v1.
Extend patchdiff_observe_scope with selections: 1–32 objects containing required
nonempty symbol_name and optional nonempty kind. Exact names and existing
qualified parent.name identities use the existing Waymark selector logic.
selections is exclusive with symbol_name, query, observation_id, top-level
kind, and a nonzero offset. No new MCP tool is registered.
The server reads one coherent target row, runs Waymark once, and resolves all
selectors from that discovery. Every selected scope must be unique, nonempty and
disjoint; adjacency is allowed, nesting/overlap is rejected. No partial bundle is
persisted. A selection failure includes selection_index (zero based) and its
code. An engine discovery failure retains the engine code.
status: "observed_batch" returns observation_bundle_id, target_file_id,
target_version, target_current_hash, waymark_engine_version, language, and
ordered scopes. Each scope contains observation_id, ordinal, symbol
(name, kind, parent), real scope_start/scope_end, observed_scope_hash,
text, truncated, and next_offset. Positions are canonical LF Unicode code
points, converted from Waymark's authoritative UTF-16 coordinates.
Selection contents total at most 65,536 code points. Initial returned text
totals at most 32,768 code points; exceeding it returns
BUNDLE_RESPONSE_TOO_LARGE, so the host can divide the selection. Each individual
page retains the existing default 4,000 / maximum 8,000 code points. Page larger
scopes through the existing observation_id + offset route. Paging is another
observation of current state; a stale, consumed or closed-cycle receipt rejects.
Atomic submission contract¶
The existing patchdiff_submit_observed_operations accepts either:
- Single form:
observation_id,operations,intent, both target guards and optional request key. Its existing absolute sequential span semantics remain. - Batch form:
observation_bundle_id, nonemptygroups,intent, bothexpected_target_versionandexpected_current_hash, andclient_request_id. Single and batch fields cannot be mixed, including explicit null fields.
Each group has exactly observation_id and nonempty operations, using the
existing typed match/span operation schema. A request allows 1–32 groups and
at most 128 operations in total. Groups may be a nonempty subset of the issued
bundle, in caller-specified order, with each observation used once.
Under the canonical target's SELECT ... FOR UPDATE lock, request-key recovery
or conflict auditing precedes fresh guard checks. Fresh admission verifies the
entire target preimage hash, all participating observation rows, bundle, target,
cycle, version/hash, unused state, original scope hashes, and structural identity
and bounds using one Waymark discovery for the locked snapshot.
Each group evaluates against its own original scope slice. match, anchors and
occurrence_index inspect that group's sequential buffer. Batch span offsets
are scope-local, beginning at zero and reflecting earlier operations in the
same group. A group cannot access a neighbor's original slice. Zero-width spans
must be strictly interior and carry both nonempty expected_before and
expected_after, matching exact adjacent text entirely inside that scope.
All groups use apply_pdcp_transaction; the global target hash is checked before
the local applier receives the actual slice hash. Successful slices are assembled
in descending original scope_start order. One accepted attempt, all accepted
observation links, all ledger rows, target content/hash and exactly one version
increment commit together. A failed group writes one rejected attempt, changes
no target/ledger state, consumes no observations, and reports group_index,
observation_id where valid, and underlying_code without unrelated target text.
Infrastructure exceptions and caller-owned rollback leave no partial acceptance.
Failure context uses scope_local_original or scope_local_intermediate_buffer
for evaluated groups. Admission failures report batch_scope_not_evaluated
without a target-text slice. A no-op warning for the entire transaction is emitted
only when the complete assembled target is unchanged.
Success returns status, one pdcp_log_id, before_version, after_version,
before_hash, after_hash, observation_bundle_id, group_count, change_count,
client_request_id, and bounded groups summaries. Complete per-operation
runtime facts remain in evidence.changes and the accepted-only ledger.
An exact keyed retry returns the original outcome with deduplicated: true, even
after a committed response was lost or the target advanced. A changed bundle,
group order/identity, operation, intent or guard with the same key produces
request_key_conflict and a separate NULL-key rejection row. Use a new key and
parent_pdcp_log_id to explicitly retry a corrected rejected proposal. Accepted
observations are consumed once. Unused members remain unconsumed but become stale
after the version increment. SAVE closes their original editing cycle.
Durable evidence and replay¶
The canonical internal record uses protocol_version = patchdiff-observed-batch/1
and runtime_version = patchdiff-observed-batch-r1. It retains ordered original
tx/2 operations and expected global guards, target and bundle IDs, intent,
editing-cycle identity and authoritative scope_evidence: observation identities,
bundle ordinals, group and operation indices, original bounds/hashes, symbol
identities, engine/language and original coordinate facts. The content hash uses
the existing canonical SHA-256 convention. The request fingerprint identifies
the original request before server evidence is attached, including the existing
parent identity rules. Existing transaction records/fingerprints are unchanged.
Durable UUID fields use canonical spelling while the original submitted spelling
remains part of request identity; an exact retry still recovers the same outcome.
Ledger metadata adds group_index, bundle_ordinal, operation_index,
observation_id, original scope_start/scope_end, and
scope_local_start/scope_local_end. span_start/span_end are local to that
operation's sequential group buffer; global change_index follows group and
operation order. Internal change IDs are prefixed g<group_index>: to keep them
unique across groups; original client IDs remain in the canonical operation list.
Migration 20261027_0005 adds nullable paired observation_bundle_id and
bundle_ordinal, unique bundle/ordinal, bundle and accepted-log indexes. A partial
unique index retains historical one-observation-per-log uniqueness for rows
without bundles. Bundled rows can link many observations to one accepted log.
Downgrade refuses any bundled rows, many-to-one links or accepted batch records;
otherwise it restores the old uniqueness and drops only the new schema fields.
Replay explicitly dispatches the batch pair, checks canonical JSON and its hash,
requires exactly the recorded authoritative links, verifies target/cycle/preimage,
bundle, order, scope coordinates and hashes, reuses the pure local applier and
descending assembly, and verifies output hash/version/count. It never calls
Waymark or changes canonical state. Tampering returns
BATCH_REPLAY_INTEGRITY_FAILURE. Historical single-scope and unobserved branches
retain their prior formats and behavior.
Batch errors¶
Observation/admission codes are INVALID_BATCH_REQUEST, BATCH_LIMIT_EXCEEDED,
BUNDLE_RESPONSE_TOO_LARGE, BUNDLE_SELECTION_NOT_FOUND,
BUNDLE_SELECTION_AMBIGUOUS, BUNDLE_SCOPE_OVERLAP, BUNDLE_SCOPE_DUPLICATE,
BUNDLE_NOT_FOUND, BUNDLE_TARGET_MISMATCH, BUNDLE_CYCLE_MISMATCH,
BUNDLE_VERSION_MISMATCH, BUNDLE_HASH_MISMATCH, BUNDLE_ALREADY_CONSUMED,
BATCH_GROUP_INVALID, BATCH_OPERATION_OUTSIDE_SCOPE,
BATCH_OPERATION_REJECTED, and BATCH_REPLAY_INTEGRITY_FAILURE.
Existing request-key, encoding, lineage and single-scope codes remain unchanged.
Complete Python, JSON and Markdown examples¶
The following executable example uses the public tool names and actual generated
schemas. The trusted operator creates targets with full; the editing client
uses observed. In a host, send these same tool names/argument objects through
tools/call in separately configured sessions. Both returned guards are passed
unchanged. Replacements deliberately change only the selected scopes.
import json
import uuid
from app.mcp.external_adapter import ExternalMCPAdapter
operator = ExternalMCPAdapter(profile="full")
editor = ExternalMCPAdapter(profile="observed")
def call(adapter, name, **arguments):
reply = adapter.call_tool(name, arguments)
assert not reply.get("isError"), reply
return json.loads(reply["content"][0]["text"])
def edit_example(filename, content, content_type, selections, operations):
target = call(operator, "patchdiff_initialize_target", filename=filename,
content=content, content_type=content_type)
tid = target["target_file_id"]
bundle = call(editor, "patchdiff_observe_scope", target_file_id=tid,
selections=selections)
assert bundle["status"] == "observed_batch", bundle
result = call(editor, "patchdiff_submit_observed_operations", target_file_id=tid,
observation_bundle_id=bundle["observation_bundle_id"],
expected_target_version=bundle["target_version"],
expected_current_hash=bundle["target_current_hash"],
client_request_id=f"example-{uuid.uuid4()}", intent="Update selected structures",
groups=[{"observation_id": scope["observation_id"], "operations": ops}
for scope, ops in zip(bundle["scopes"], operations)])
assert result["status"] == "accepted" and result["after_version"] == 1, result
assert call(editor, "patchdiff_save", target_file_id=tid)["status"] == "saved"
assert call(editor, "patchdiff_replay", target_file_id=tid)["materialized_state_matches"]
return call(operator, "patchdiff_export_target", target_file_id=tid)["content"]
python_output = edit_example("quotas.py",
"def api_quota():\n return 100\n\ndef etl_quota():\n return 100\n\ndef ui_quota():\n return 100\n",
"text/x-python",
[{"symbol_name": name, "kind": "function"} for name in ("api_quota", "etl_quota", "ui_quota")],
[[{"op": "match", "match": "return 100", "replace": "return 200"}] for _ in range(3)])
assert python_output.count("return 200") == 3
json_output = edit_example("quotas.json",
json.dumps({"tenants": [{"id": name, "quota": 100} for name in ("api", "etl", "ui")]}, indent=2) + "\n",
"application/json", [{"symbol_name": f"tenants[{name}]"} for name in ("api", "etl", "ui")],
[[{"op": "match", "match": '"quota": 100', "replace": '"quota": 200'}] for _ in range(3)])
assert all(tenant["quota"] == 200 for tenant in json.loads(json_output)["tenants"])
markdown_output = edit_example("release.md",
"# Release\n\n## Deploy\nstate: old\n\n## Rollback\nstate: old\n\n## Verification\nstate: old\n",
"text/markdown", [{"symbol_name": name} for name in ("## Deploy", "## Rollback", "## Verification")],
[[{"op": "match", "match": "state: old", "replace": "state: ready"}] for _ in range(3)])
assert markdown_output.count("state: ready") == 3
Deterministic capability evidence¶
Discovery can use the internal resident Waymark worker through trusted startup configuration. It retains fresh locked verification and the existing transaction schemas, catalogs and replay profiles.
Run python backend/tests/verification/structural_batch/verify.py --output <new-directory> with
live Postgres and pinned Waymark dependencies. The runner issues real stdio MCP
requests and counts tools/call lines separately for trusted lifecycle and
editing clients. Its pinned declarative operation fixture was extracted from the
archived exploratory transcript. CI retains only the required original five-file
inputs and independent oracle under backend/tests/verification/; internal
transcripts, controllers, captures and the Gate 2 manifest remain unchanged in the
ignored local evaluation/ directory. Full-byte equality precedes the 20 behavioral checks.
The original exploratory C recorded 25 observations (5 discovery + 20 selected), 20 editing submissions, 1 search, 5 SAVE and 5 replay: 56 MCP calls. With known selectors, the deterministic batch path requires 5 bundled observations, 5 submissions, 5 SAVE and 5 replay: 20 editing calls. Initialization, export and audit inspection are counted separately as trusted lifecycle work. API-call reduction establishes capability; it establishes no model latency, context, token-cost or comparative performance result. Gate 2 remains closed, and a new direct three-arm run still requires owner authorization.
Versioned external view¶
Opt-in observed_v2 separates candidate listing, selection, paging and bundle commit into five external purpose-specific tools. It lowers unchanged values into the structural batch services described here. See the v2 contract; existing profile catalogs and durable formats remain unchanged.