Skip to content

One SDK write operation for every write path - #43

Merged
igor-ctrl merged 1 commit into
mainfrom
refactor/sdk-write-operation
Oct 30, 2026
Merged

igor-ctrl merged 1 commit into
mainfrom
refactor/sdk-write-operation

Conversation

@igor-ctrl

@igor-ctrl igor-ctrl commented Oct 30, 2026 •

Copy link
Copy Markdown
Owner

Summary

Every mutation now goes through the SDK: client.plan() resolves the URL once, and client.write() sends the request, writes the audit entry, and returns the status and BC correlation id. CLI post/patch/delete/action, batch steps and rollback, SafeContext, MCP record writes and plain client.post() all share this one path.

  • Correlation ID fixed. It was always empty: bcli read x-ms-correlation-request-id, a header BC never sends. It now reads request-id, the value BC quotes as CorrelationId in its own error messages. Audit entries, result envelopes and batch ledger rows now carry the real status and correlation ID.
  • One URL resolver. Public client.resolve() replaces the private _resolve_url. get --company all now lets an explicit --publisher win over the registry route and follows pages with --all.
  • New client.pages(). get --all and saved queries keep --top, --skip and --count.
  • Batch. Batch writes and rollback deletes are now audited. Steps accept publisher/group/version, and dry-run shows each step's URL.
  • MCP. bcli_post, bcli_patch and bcli_delete run in-process. The other tools still start a bcli subprocess, but no longer block the event loop.
  • ETL. The dlt source uses bcli's transport and respects disable_standard_api.

Behaviour changes are listed in CHANGELOG.md under Unreleased.

Test plan

  • uv run pytest: 1257 passed. Tests now run the real client against a shared FakeTransport instead of AsyncMock clients.
  • uv run ruff check src tests
  • Run against the SBEnvQD sandbox: reads, --all/--skip, -c all, saved query, dry-runs, CLI post→patch→delete, batch run plus rollback, MCP post/delete. All test records were cleaned up.

🤖 Generated with Claude Code


Note

Medium Risk
Centralizes every mutation and audit emission in the SDK; regressions could affect URL routing, dry-run parity, batch ledger metadata, or MCP write behavior across many entry points.

Overview
All BC mutations now share one SDK path: client.plan() resolves the URL once (so dry-run and real runs match), and client.write() executes the HTTP call, emits audit entries when enabled, and returns WriteResult with status and correlation id. CLI post/patch/delete/action, batch steps and rollback, SafeContext, MCP record writes, and direct client.post/patch/delete all route through this.

Audit and debugging: Write auditing moved from CLI-only wrappers into AsyncBCClient.write for profile-based clients, with caller set to cli, mcp, or sdk. The BC correlation id is read from the request-id header (not the unused x-ms-correlation-request-id), so audit logs, result envelopes, and batch ledger rows now record real HTTP status and correlation ids.

API and read fixes: _resolve_url is replaced by public client.resolve() and new client.pages(); sync BCClient exposes the same. get --company all respects explicit --publisher/group/version and can paginate with --all; get --all and saved queries with all_pages keep $top, $skip, and $count.

MCP and ETL: bcli_post/patch/delete run in-process via the SDK (same audit/correlation behavior); other subprocess tools use asyncio.to_thread. The dlt bridge uses BCTransport and the profile registry’s disable_standard_api / allow-lists.

Reviewed by Cursor Bugbot for commit ff5788d. Bugbot is set up for automated code reviews on this repo. Configure here.

Every mutation now goes through AsyncBCClient.plan() (resolve once) and
AsyncBCClient.write() (send, audit, return status + correlation id): CLI
post/patch/delete/action, batch steps and rollback, SafeContext, MCP record
writes, and plain SDK client.post().

- Transport returns BCResponse(body, status, correlation_id); the BC
  correlation id is read from `request-id` (BC never sent the
  x-ms-correlation-request-id header we looked for, so it was always empty).
- Public client.resolve() replaces private _resolve_url everywhere;
  `get --company all` no longer lets the registry override --publisher and
  pages with --all.
- client.pages(entity, query, company_id=) keeps top/skip/count for
  `get --all` and saved queries.
- Batch writes and rollback deletes are audited; ledger rows carry the real
  correlation id; steps accept publisher/group/version.
- MCP bcli_post/patch/delete run in-process via the SDK; remaining subprocess
  tools run off the event loop.
- The dlt source runs on bcli's transport and honours disable_standard_api.
- Tests drive the real client against a shared FakeTransport instead of
  AsyncMock clients.

Verified against the SBEnvQD sandbox (reads, dry-runs, CLI/batch/MCP writes,
rollback, audit and envelopes).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes using default effort and found 2 potential issues.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit ff5788d. Configure here.

return None
try:
base = client._resolve_url(entity) # noqa: SLF001
return client.resolve(entity, record_id=str(record_id))

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Batch rollback misses custom route

Medium Severity

A batch POST that uses publisher/group/version writes to that custom route, but _compose_rollback_url calls resolve without those fields, so the ledger stores a standard or registry URL. batch rollback then DELETEs a different API than the create, which 404s or can remove the wrong record. The write already has the planned URL; rollback composition never receives the step route.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit ff5788d. Configure here.

return client.plan(
action.upper(), step.get("endpoint", ""),
body=None if action == "delete" else (step.get("data") or {}),
record_id=record_id,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Batch POST treats id as key

Medium Severity

_plan_step always forwards a step's id into plan(..., record_id=...), including for action: post. The previous batch runner posted to the collection and ignored id on creates. A POST step that also has id now hits entity(id) instead of entity, so the create goes to the wrong URL.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit ff5788d. Configure here.

@igor-ctrl
igor-ctrl merged commit e827682 into main Oct 30, 2026
4 checks passed
@igor-ctrl
igor-ctrl deleted the refactor/sdk-write-operation branch October 30, 2026 14:35
@linear-code

linear-code Bot commented Oct 30, 2026

Copy link
Copy Markdown

TECH-1283

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant