docs(m8): Transport site/instance transport, name-mapping, Myers diff, stale enumeration (M8 INT)

This commit is contained in:
Joseph Doherty
2026-06-18 07:53:25 -04:00
parent f881521cc9
commit 4d888c63a3
7 changed files with 136 additions and 45 deletions
+2 -1
View File
@@ -88,7 +88,7 @@ Related repos cloned as sibling directories under `~/Desktop/` — referenced fo
21. Notification Outbox — Central component ingesting store-and-forwarded notifications, `Notifications` audit table, dispatcher loop, retry/parking, delivery KPIs.
22. Site Call Audit — Central component auditing site cached calls (`CachedCall`/`CachedWrite`); `SiteCalls` audit table, telemetry ingest, reconciliation, KPIs, central→site Retry/Discard relay; sites remain the source of truth.
23. Audit Log — Central append-only AuditLog table spanning every script-trust-boundary action (outbound API sync+cached, outbound DB sync+cached, notifications, inbound API). Site SQLite hot-path + gRPC telemetry + reconciliation; combined telemetry with Site Call Audit; central direct-write for Notification Outbox dispatch + Inbound API; monthly partitioning, 365-day retention.
24. Transport — File-based, encrypted bundle export/import via Central UI. Templates, system artifacts, central-only configuration. Per-conflict resolution. Correlated audit via `BundleImportId`. No site involvement.
24. Transport — File-based, encrypted bundle export/import via Central UI. Templates, system artifacts, central-only configuration, plus site/instance-scoped config (`Site`s, site `DataConnection`s, `Instance`s) reconciled across environments by a `BundleNameMap` name-mapping subsystem. Per-conflict resolution with a per-line Myers diff. Correlated audit via `BundleImportId`. Never touches site runtime nodes (imported instances land `NotDeployed`).
25. Script Analysis — Shared authoritative script-trust analyzer: unified forbidden-API deny-list (`ScriptTrustPolicy`), fused semantic+syntactic validator (`ScriptTrustValidator`), Roslyn compile wrapper (`RoslynScriptCompiler`), and compile-only globals stubs (`ScriptCompileSurface`/`TriggerCompileSurface`); consumed by Template Engine, Site Runtime, Inbound API, and Central UI.
26. KPI History — Reusable central KPI-history backbone: tall/EAV `KpiSample` store in central MS SQL, `KpiHistoryRecorderActor` cluster singleton (`kpi-history-recorder`, not readiness-gated) sampling DI-registered `IKpiSampleSource`s every minute, bucketed query (`GetRawSeriesAsync` + `KpiSeriesBucketer`) + scoped `KpiHistoryQueryService`, and a reusable custom-SVG `KpiTrendChart`; ships trends for Notification Outbox, Site Call Audit, Audit Log, and Site Health.
@@ -141,6 +141,7 @@ Related repos cloned as sibling directories under `~/Desktop/` — referenced fo
- Last-write-wins for concurrent template editing (no optimistic concurrency on templates).
- Optimistic concurrency on deployment status records.
- Naming collisions in composed feature modules are design-time errors.
- Transport (#24, M8): bundles now move site/instance config (`Site`s, site-scoped `DataConnection`s, `Instance`s + override children + `Area` by name), not just central-only config — but still never touch site runtime nodes (imported instances land `NotDeployed`). A `BundleNameMap` name-mapping subsystem (Commons: `SiteMapping`/`ConnectionMapping`/`MappingAction`) reconciles environment-specific identifiers (sites by `SiteIdentifier`, connections by site-qualified `{SiteIdentifier}/{Name}`, instances by `UniqueName`): auto-match → operator override via the import-wizard Map step / CLI `--map-site`/`--map-connection`/`--create-missing-*` → blocker rows for the unresolved. D3 "carry full config (encrypted secrets)": Site addresses travel; `DataConnection` config rides the encrypted `SecretsBlock` (presence-only in diffs). Per-line Myers diff for code fields via the pure `LineDiffer` (`ArtifactDiff` embeds a size-capped structured `lineDiff`); `ImportResult.StaleInstanceIds` is real via the `IStaleInstanceProbe` seam (Commons; implemented in DeploymentManager). `schemaVersion` 1.0→1.1 (additive); `bundleFormatVersion` stays 1; no new EF tables/columns.
### Store-and-Forward
- Fixed retry interval, no max buffer size. Only transient failures buffered.
+1 -1
View File
@@ -99,7 +99,7 @@ Both stacks share the infrastructure services in [`infra/`](infra/) (MS SQL, LDA
| 21 | Notification Outbox | [docs/requirements/Component-NotificationOutbox.md](docs/requirements/Component-NotificationOutbox.md) | Central component ingesting store-and-forwarded notifications into the `Notifications` audit table, with `NotificationOutboxActor` singleton dispatcher, per-type delivery adapters, retry/parking, status tracking, daily purge, and delivery KPIs. |
| 22 | Site Call Audit | [docs/requirements/Component-SiteCallAudit.md](docs/requirements/Component-SiteCallAudit.md) | Central component auditing site cached calls (`ExternalSystem.CachedCall`/`Database.CachedWrite`) into the `SiteCalls` audit table, with `SiteCallAuditActor` singleton, telemetry ingest, periodic reconciliation, point-in-time KPIs, daily purge, and central→site Retry/Discard relay for parked calls. |
| 23 | Audit Log | [docs/requirements/Component-AuditLog.md](docs/requirements/Component-AuditLog.md) | New central append-only AuditLog spanning every script-trust-boundary action (outbound API sync+cached, outbound DB sync+cached, notifications, inbound API). Site-local SQLite hot-path append + gRPC telemetry + central reconciliation; combined telemetry packet with Site Call Audit; central direct-write for Notification Outbox dispatch + Inbound API middleware; monthly partitioning, 365-day default retention. |
| 24 | Transport | [docs/requirements/Component-Transport.md](docs/requirements/Component-Transport.md) | Bundle export/import for templates, shared scripts, external systems, central-only artifacts. AES-256-GCM encryption; per-conflict resolution on import; correlated audit trail. |
| 24 | Transport | [docs/requirements/Component-Transport.md](docs/requirements/Component-Transport.md) | Bundle export/import for templates, shared scripts, external systems, central-only artifacts, plus site-scoped config — `Site`s, site `DataConnection`s, and `Instance`s — reconciled across environments by a `BundleNameMap` name-mapping subsystem (auto-match + Map wizard step / CLI flags). AES-256-GCM encryption (carry-full-config with encrypted connection secrets); per-conflict resolution with a per-line Myers diff for code fields; real stale-instance enumeration; correlated audit trail. Never touches site runtime nodes. |
| 25 | Script Analysis | [docs/requirements/Component-ScriptAnalysis.md](docs/requirements/Component-ScriptAnalysis.md) | Shared authoritative script-trust analyzer: unified forbidden-API deny-list (`ScriptTrustPolicy`), fused semantic+syntactic validator (`ScriptTrustValidator`), Roslyn compile wrapper (`RoslynScriptCompiler`), and compile-only globals stubs (`ScriptCompileSurface`/`TriggerCompileSurface`); consumed by Template Engine, Site Runtime, Inbound API, and Central UI. |
| 26 | KPI History | [docs/requirements/Component-KpiHistory.md](docs/requirements/Component-KpiHistory.md) | Reusable central KPI-history backbone: tall/EAV `KpiSample` store (central MS SQL), `KpiHistoryRecorderActor` cluster singleton (`kpi-history-recorder`, not readiness-gated) sampling DI-registered `IKpiSampleSource`s each minute, bucketed `GetRawSeriesAsync` + `KpiSeriesBucketer` query, and a reusable custom-SVG `KpiTrendChart`. Ships trends for Notification Outbox, Site Call Audit, Audit Log, and Site Health. |
@@ -94,8 +94,13 @@ Delivered per `docs/plans/2026-06-18-m7-opcua-mxgateway-ux-design.md` (full scop
Small follow-ups logged (not blocking): stamp `SourceNode` on the `SecuredWrite` audit rows (currently NULL); an aggregated **live** alarm stream for the summary page (snapshot + poll today); central-persisted, auditable cert trust (site-local today).
#### M8 — Transport (T18, T20)
Site-scoped / instance-scoped artifact transport (name-mapping subsystem); per-line/Myers diff for Modified artifacts.
#### M8 — Transport (T18, T20) — **DELIVERED**
Delivered both features, with the silent gap #16 folded in:
- **T18** — site/instance-scoped artifact transport. Transport now also moves `Site` definitions, site-scoped `DataConnection`s (protocol connections), and `Instance`s (+ `InstanceAttributeOverride`/`InstanceAlarmOverride`/`InstanceNativeAlarmSourceOverride`/`InstanceConnectionBinding` children + `Area` by name). New **name-mapping subsystem** (`BundleNameMap`/`SiteMapping`/`ConnectionMapping`/`MappingAction` in Commons): auto-match (sites by `SiteIdentifier`, connections by the site-qualified `{SiteIdentifier}/{Name}`, instances by `UniqueName`), operator override via the Central UI import **Map step** (between Passphrase and Diff) or CLI `--map-site`/`--map-connection`/`--create-missing-sites`/`--create-missing-connections`; export gains `--sites`/`--instances` + a UI Sites & Instances picker. D3 **carry-full-config (encrypted secrets)**: Site addresses travel; `DataConnection` `PrimaryConfiguration`/`BackupConfiguration` ride the encrypted `SecretsBlock` (presence-only in diffs). Apply resolves/creates target sites + connections in one EF transaction, upserts instances forced `NotDeployed`, and rewires FKs (`InstanceConnectionBinding.DataConnectionId` via the connection map with a Pass-2; `NativeAlarmSourceOverride.ConnectionNameOverride` to the mapped target). `schemaVersion` 1.0→1.1 (additive); `bundleFormatVersion` stays 1; **no new EF tables/columns**.
- **T20** — per-line **Myers** diff. New pure `LineDiffer` (custom O(ND), no third-party lib) replaces the coarse `<N lines>` marker for code fields; `ArtifactDiff` embeds a size-capped structured `lineDiff` (hunks + `truncated` + add/remove totals); the import wizard renders Modified code fields via the shared `LineDiffView` component.
- **#16 folded in** — `ImportResult.StaleInstanceIds` is no longer a stub: for each Overwritten template the importer enumerates deployed target instances whose freshly-flattened revision hash drifts from `DeployedConfigSnapshot.RevisionHash` via the new `IStaleInstanceProbe` seam (Commons; implemented in DeploymentManager), so Confirm shows a real count and Result deep-links the filtered Deployments page.
Small follow-ups logged (not blocking): DeploymentManagerRepository hydration for the stale probe; large-bundle/perf hardening. **T19** (direct cluster-to-cluster pull / asymmetric bundle signing / differential bundles) remains **deferred** to its own brainstorm.
#### M9 — Templates & authoring (T22T26, T28, T30T32)
Template tree search/filter; folder drag-drop + sibling reorder + root context menu; move data connection between sites; connection live-status indicators; base-template versioning "update-derived" flow + multi-level inheritance; strict expression-trigger analysis kind; schema-driven value-entry forms + hover/completion + JSON Schema `$ref`/library; CLI Retry/Discard for cached calls; unified notifications+site-calls outbox page.
+8 -2
View File
@@ -325,11 +325,17 @@ scadabridge api-method delete --id <id>
### Bundle Commands (Transport #24)
```
scadabridge bundle export --output <path> [--passphrase <phrase>] [--all] [--include-dependencies] [--templates <names>] [--shared-scripts <names>] [--external-systems <names>] [--db-connections <names>] [--notification-lists <names>] [--smtp-configs <names>] [--api-methods <names>] [--source-environment <env>]
scadabridge bundle export --output <path> [--passphrase <phrase>] [--all] [--include-dependencies] [--templates <names>] [--shared-scripts <names>] [--external-systems <names>] [--db-connections <names>] [--notification-lists <names>] [--smtp-configs <names>] [--api-methods <names>] [--sites <ids|names>] [--instances <unique-names>] [--source-environment <env>]
scadabridge bundle preview --input <path> [--passphrase <phrase>]
scadabridge bundle import --input <path> [--passphrase <phrase>] [--on-conflict skip|overwrite|rename]
scadabridge bundle import --input <path> [--passphrase <phrase>] [--on-conflict skip|overwrite|rename] [--map-site <src=dst> ...] [--map-connection <srcSite/srcName=dstName> ...] [--create-missing-sites] [--create-missing-connections]
```
`--sites` / `--instances` (M8/T18) pull site-scoped artifacts: a `--sites` token is a `SiteIdentifier` (preferred) or friendly name; a `--instances` token is a `UniqueName`. Selecting an instance also pulls its site and bound `DataConnection`s.
`bundle preview` prints the per-row diff plus a **required-mapping summary**: for each source site/connection it states either the auto-match (`auto-matches 'X'`) or the directive to supply `--map-site` / `--map-connection` or `--create-missing-*`.
On import, the mapping flags reconcile environment-specific identifiers. `--map-site` / `--map-connection` are repeatable; a token with no `=dst` (or `=` with an empty right-hand side) means **create-new** from the bundle payload, otherwise it binds the source to the named existing target. `--create-missing-sites` / `--create-missing-connections` create any still-unmapped source site/connection instead of aborting. Imported instances always land `NotDeployed`.
Inbound API keys are not transported between environments — re-create them on the destination via CLI or UI.
Bundle commands use a 5-minute timeout.
+97 -30
View File
@@ -2,7 +2,9 @@
## Purpose
The Transport component provides a file-based, encrypted, environment-agnostic way to promote configuration artifacts from one ScadaBridge cluster to another through the Central UI. A user with the Design role on the source cluster exports a selected set of templates and supporting artifacts to a `.scadabundle` file. A user with the Admin role on the target cluster uploads the bundle, reviews a diff, resolves conflicts per artifact, and applies it. Import is config-only: it updates the central configuration database; affected instances surface as stale on the existing Deployments page and the user redeploys via the standard flow. Transport does not touch site nodes, does not move runtime state, and does not move site-scoped artifacts.
The Transport component provides a file-based, encrypted, environment-agnostic way to promote configuration artifacts from one ScadaBridge cluster to another through the Central UI. A user with the Design role on the source cluster exports a selected set of templates and supporting artifacts to a `.scadabundle` file. A user with the Admin role on the target cluster uploads the bundle, reviews a diff, resolves conflicts per artifact, and applies it. Import is config-only: it updates the central configuration database; affected instances surface as stale on the existing Deployments page and the user redeploys via the standard flow. Transport does not touch site runtime nodes and does not move runtime state.
As of M8 (T18), Transport is no longer limited to central-only configuration: it also moves **site-scoped configuration**`Site` definitions, site-scoped `DataConnection`s (protocol connections), and `Instance`s with their override children and area membership. Because site identifiers and connection names differ across environments, a **name-mapping subsystem** (see "Name Mapping") reconciles source references onto target sites/connections at import time. This is still purely a central-configuration-database operation: Transport never deploys to or otherwise touches site cluster nodes — newly imported instances land as `NotDeployed`, and the operator redeploys via the existing pipeline.
## Location
@@ -16,6 +18,9 @@ The Transport component provides a file-based, encrypted, environment-agnostic w
- Define and own the `.scadabundle` file format (ZIP container, `manifest.json`, `content.json` / `content.enc`).
- Resolve artifact dependencies at export time: base templates, shared scripts, external systems, template folders, notification lists, SMTP configs, API keys, API methods.
- Move **site-scoped configuration** (T18): `Site` definitions, site-scoped `DataConnection`s (protocol connections — distinct from External-System `DatabaseConnection`s), and `Instance`s along with their `InstanceAttributeOverride` / `InstanceAlarmOverride` / `InstanceNativeAlarmSourceOverride` / `InstanceConnectionBinding` children and `Area` membership (carried by name).
- Reconcile cross-environment site identifiers and connection names through the **name-mapping subsystem** (`BundleNameMap`): auto-match by identifier/name, operator override via the import-wizard Map step or CLI flags, and per-conflict create-or-bind resolution (see "Name Mapping").
- Compute a **per-line (Myers) diff** for code fields on Modified artifacts (T20) via the pure `LineDiffer`, embedding a size-capped structured line diff in each `ArtifactDiff`.
- Serialize and deserialize all transportable entity types to/from bundle DTOs, carving secret fields into an isolated `SecretsBlock`.
- Encrypt content with AES-256-GCM + PBKDF2-SHA256 (600 000 iterations) when a passphrase is supplied; leave content plaintext with a UI warning and an `UnencryptedBundleExport` audit event when none is given.
- Validate `manifest.json` on upload: format version gating, SHA-256 content hash verification.
@@ -49,7 +54,7 @@ Exactly one of `content.json` or `content.enc` is present.
```json
{
"bundleFormatVersion": 1,
"schemaVersion": "1.0",
"schemaVersion": "1.1",
"createdAtUtc": "2026-05-24T12:34:56Z",
"sourceEnvironment": "dev-cluster-a",
"exportedBy": "alice@corp.example",
@@ -65,13 +70,19 @@ Exactly one of `content.json` or `content.enc` is present.
"summary": {
"templates": 12, "templateFolders": 3, "sharedScripts": 4,
"externalSystems": 2, "dbConnections": 1,
"notificationLists": 1, "smtpConfigs": 0, "apiKeys": 2, "apiMethods": 5
"notificationLists": 1, "smtpConfigs": 0, "apiKeys": 2, "apiMethods": 5,
"sites": 2, "dataConnections": 3, "instances": 8
},
"contents": [
{ "type": "Template", "name": "Pump", "version": 5,
"dependsOn": ["SharedScript:PumpUtils"] },
{ "type": "Template", "name": "Pump.WaterPump", "version": 3,
"dependsOn": ["Template:Pump", "ExternalSystem:HistorianAPI"] }
"dependsOn": ["Template:Pump", "ExternalSystem:HistorianAPI"] },
{ "type": "Site", "name": "site-a", "version": 1, "dependsOn": [] },
{ "type": "DataConnection", "name": "site-a/PlantOpcUa", "version": 1,
"dependsOn": ["Site:site-a"] },
{ "type": "Instance", "name": "WaterPump-01", "version": 1,
"dependsOn": ["Template:Pump.WaterPump", "Site:site-a"] }
]
}
```
@@ -80,8 +91,8 @@ The manifest is plaintext so the import wizard can preview bundle contents and s
### `content.json` / `content.enc`
- One top-level array per entity type, POCO shapes serialized via `System.Text.Json`.
- Secret fields (API key hashes, SMTP password, external system credentials, DB connection passwords) live in a nested `secrets` block on each affected entity.
- One top-level array per entity type, POCO shapes serialized via `System.Text.Json`. As of schema 1.1 (T18) this includes `sites`, `dataConnections`, and `instances` arrays (`SiteDto`, `DataConnectionDto`, `InstanceDto` + its override-child and connection-binding DTOs) alongside the existing central-config arrays.
- Secret fields (API key hashes, SMTP password, external system credentials, DB connection passwords) live in a nested `secrets` block on each affected entity. Per the D3 "carry full config (encrypted secrets)" decision, a `DataConnection`'s `PrimaryConfiguration` / `BackupConfiguration` (endpoint + credentials) also ride inside this encrypted `SecretsBlock`; they appear in diffs as presence-only and are never echoed in plaintext. A `Site`'s `NodeA`/`NodeB` and `GrpcNodeA`/`GrpcNodeB` addresses travel as ordinary (non-secret) `SiteDto` fields.
- The whole `content` blob is AES-256-GCM encrypted with a key derived via PBKDF2-SHA256 (600 000 iterations) from the user's passphrase, with per-bundle random salt and per-encryption random IV.
- Unencrypted bundles are allowed but the UI warns and audit-tags them `UnencryptedBundleExport`.
@@ -89,6 +100,7 @@ The manifest is plaintext so the import wizard can preview bundle contents and s
- Unknown top-level entity types in `contents[]` surface in the import preview as "skipped — unsupported in this version" rather than failing the whole import.
- `bundleFormatVersion` newer than what the importer supports produces a hard refusal at upload with a clear upgrade message.
- `schemaVersion` minor increments are **additive-only**. T18 bumped it `1.0 → 1.1` (adding the `sites`/`dataConnections`/`instances` arrays). `bundleFormatVersion` stays `1`; `ManifestValidator` gates solely on `bundleFormatVersion`, so a pre-T18 importer still accepts a 1.1 bundle and simply lists the new site/instance/connection entries as "skipped — unsupported in this version".
## Architecture
@@ -119,7 +131,7 @@ flowchart TD
class T muted
```
The component is central-only. It is registered in `ZB.MOM.WW.ScadaBridge.Host` for central roles only, never for site roles. All persistence flows through existing audited repository interfaces in `ZB.MOM.WW.ScadaBridge.ConfigurationDatabase` — the component does not call `DbContext.SaveChangesAsync` directly. `BundleSessionStore` is in-process on the active central node (matching Blazor Server circuit affinity): 30-minute TTL, eviction on expiry, 3-strike passphrase lockout per session.
The component is central-hosted. It is registered in `ZB.MOM.WW.ScadaBridge.Host` for central roles only, never for site roles — even when a bundle carries site-scoped config (T18), Transport only writes the central configuration database, never the site clusters. All persistence flows through existing audited repository interfaces in `ZB.MOM.WW.ScadaBridge.ConfigurationDatabase` — the component does not call `DbContext.SaveChangesAsync` directly. `BundleSessionStore` is in-process on the active central node (matching Blazor Server circuit affinity): 30-minute TTL, eviction on expiry, 3-strike passphrase lockout per session.
## Export Flow
@@ -127,6 +139,8 @@ The component is central-only. It is registered in `ZB.MOM.WW.ScadaBridge.Host`
**Step 1 — Select artifacts.** Templates are rendered as a tree matching the existing Templates page (the `TemplateFolderTree.razor` shared component, used in its new checkbox-selection mode). Tri-state checkboxes on folders (`☑` all, `☐` none, `▣` partial). Search filters the tree in place. Other artifact groups (shared scripts, external systems, notification lists, SMTP configs, API keys, API methods) are flat checkbox lists.
A **Sites & Instances** section (T18) adds a flat list of sites, each row expandable to its instances; selecting a site or individual instances pulls them (and their site-scoped `DataConnection`s) into the bundle. The wizard distinguishes operator-seeded selections from artifacts auto-included by dependency resolution (e.g., an instance's site and bound connections).
**Step 2 — Review dependencies.** The resolver expands the user's selection along these edges:
- `Template A` composes `Template B` → include `B`.
@@ -195,27 +209,29 @@ Authorization: `RequireDesign` on both the Razor page and `IBundleExporter.Expor
## Import Flow
### UI — 5-Step Wizard (Admin nav group)
### UI — 6-Step Wizard (Admin nav group)
**Step 1 — Upload.** Drag-and-drop or browse. On selection, the manifest is parsed and displayed (source env, exporter, timestamp, content count, SHA-256, encrypted yes/no). The manifest hash is validated against the `content` blob.
**Step 2 — Passphrase** (skipped if the bundle is unencrypted). 3-wrong-attempt lockout invalidates the session.
**Step 3 — Diff & resolve conflicts.** For each artifact in the bundle, compare to existing by name:
**Step 3 — Map** (T18; shown only when the bundle carries sites and/or site-scoped connections). For every source `Site` and every source `(site, connection)` the importer first auto-matches against the target environment (sites by `SiteIdentifier`, connections by the site-qualified `{SiteIdentifier}/{Name}` key). The operator confirms or overrides each row: **Map to existing** (bind to a target site/connection) or **Create new** (materialize it from the bundle payload). Any source reference that neither auto-matches nor is set to Create-new becomes a **blocker row** and Apply stays disabled until resolved. The Map step renders only the rows that still require a decision; fully auto-matched references pass through silently.
**Step 4 — Diff & resolve conflicts.** For each artifact in the bundle, compare to existing by name:
- **Identical** (field-by-field) → marked, auto-skipped, cannot be selected.
- **Modified** → shows `+/-/~` line diff. User picks Skip / Overwrite / Rename.
- **Modified** → shows a real per-line `+/-` diff for code fields (`TemplateScript.Code`, `SharedScript.Code`, `ApiMethod.Script`), rendered by the shared `LineDiffView` component from the structured `lineDiff` payload (T20). User picks Skip / Overwrite / Rename.
- **New** → marked `+ Add`. User can opt to skip individually.
Bulk "Apply to all" at the top (Overwrite / Skip / Rename), overridable per row. Summary line at the bottom: "5 add · 2 overwrite · 1 skip · 0 rename".
Bundle references that cannot be satisfied in either the bundle or the target DB (e.g., a template references a shared script that is neither in the bundle nor pre-existing) appear as **blocker rows** — Apply is disabled until they are resolved (typically by skipping the dependent artifact or re-exporting with dependencies).
Bundle references that cannot be satisfied in either the bundle or the target DB (e.g., a template references a shared script that is neither in the bundle nor pre-existing) appear as **blocker rows** — Apply is disabled until they are resolved (typically by skipping the dependent artifact or re-exporting with dependencies). Unmapped/unmatched sites and connections that the operator did not set to Create-new also surface as blocker rows here, mirroring the Step-3 Map decisions.
**Blocker-scan heuristic boundaries.** The scanner walks `TemplateScript.Code`, `TemplateAttribute.Value`, and `ApiMethod.Script` looking for top-level `Identifier(` or `Identifier.` tokens. To keep the heuristic usable on real script bodies it (a) skips identifiers preceded by `.` (member access — `obj.Method()` does not flag `Method`); (b) does NOT scan `TemplateAttribute.DataSourceReference` (an OPC UA address path, never script source); and (c) filters out a small `KnownNonReferenceNames` denylist of .NET stdlib types (`Convert`, `DateTimeOffset`, `ToString`, `Dispose`, `UtcNow`, …), ScadaBridge runtime API roots (`Notify`, `Database`, `ExternalSystem`, `Scripts`, `Instance`, `Parameters`, `Attributes`, `Route`, …), and common SQL keywords that appear inside string literals (`COUNT`, `SELECT`, `FROM`, …). Both the diff-step `DetectBlockersAsync` and the Apply-time `RunSemanticValidationAsync` Pass 1 share this filter, so the diff preview and the Apply gate agree.
**Step 4 — Confirm.** Final summary plus a "N instances will become stale" warning enumerating affected instances. User types the source environment name to confirm (typo-resistant gate at the prod boundary).
**Step 5 — Confirm.** Final summary plus a "N instances will become stale" warning enumerating affected instances (a real count as of M8 — see "Stale-Instance Signaling"). User types the source environment name to confirm (typo-resistant gate at the prod boundary).
**Step 5 — Result.** Counts, link to the `BundleImported` audit row, link to the Deployments page filtered to the newly stale instances.
**Step 6 — Result.** Counts, link to the `BundleImported` audit row, link to the Deployments page filtered to the newly stale instances.
### Backend
@@ -226,8 +242,8 @@ flowchart TD
LOAD["IBundleImporter.LoadAsync<br/>· verify SHA-256 (manifest vs content)<br/>· check bundleFormatVersion supported<br/>· decrypt content.enc with passphrase (if encrypted)<br/>· deserialize entities<br/>· open BundleSession (30-min TTL)"]
PREVIEW["PreviewAsync → diff vs target DB → ImportPreview"]
REVIEW["(user reviews + resolves conflicts)"]
APPLY["ApplyAsync (single EF transaction)<br/>· run two-tier semantic validation<br/>&nbsp;&nbsp;(minimal name scan + full SemanticValidator)<br/>· apply resolutions (add / overwrite / skip / rename)<br/>· upsert TemplateFolder hierarchy<br/>· IAuditService.LogAsync(BundleImported …)<br/>· commit"]
RESULT["ImportResult → UI step 5"]
APPLY["ApplyAsync(resolutions, BundleNameMap?) — single EF transaction<br/>· run two-tier semantic validation<br/>&nbsp;&nbsp;(minimal name scan + full SemanticValidator)<br/>· resolve/create target sites + connections (name map)<br/>· apply resolutions (add / overwrite / skip / rename)<br/>· upsert TemplateFolder hierarchy<br/>· upsert instances (forced NotDeployed); rewire FKs<br/>· IAuditService.LogAsync(BundleImported …)<br/>· commit"]
RESULT["ImportResult → UI step 6"]
DEPLOYMENTS["'View on Deployments →' (existing page)"]
USER --> LOAD
@@ -253,9 +269,31 @@ flowchart TD
Authorization: `RequireAdmin` on both the Razor page and `IBundleImporter.*` entrypoints.
### Name Mapping
Site identifiers and connection names are environment-specific, so a bundle exported from `dev-cluster-a` cannot assume those identifiers exist (or mean the same thing) on the target. The **name-mapping subsystem** reconciles source → target references at import time.
**Model (`ZB.MOM.WW.ScadaBridge.Commons`).** `BundleNameMap` carries two collections:
- `SiteMapping(SourceSiteIdentifier, Action, TargetSiteIdentifier?)` — per source site.
- `ConnectionMapping(SourceSiteIdentifier, SourceConnectionName, Action, TargetConnectionName?)` — per source `(site, connection)`.
`Action` is the `MappingAction` enum: **`MapToExisting`** (bind the source reference to a named target site/connection, honouring the chosen conflict resolution) or **`CreateNew`** (materialize the site/connection from the bundle payload — full config, including the encrypted `DataConnection` secrets per D3). `BundleNameMap.Empty` is the no-mapping default for central-only bundles.
**Identity keys.** Sites are matched by `SiteIdentifier`; connections by the **site-qualified** key `{SiteIdentifier}/{Name}`; instances by `UniqueName`.
**Auto-match → operator override → apply.** The importer first auto-matches every source site and connection against the target by these identity keys. The operator confirms/overrides via the Central UI **Map step** (Step 3) or the CLI `--map-site` / `--map-connection` / `--create-missing-*` flags. References that neither auto-match nor are set to Create-new become **blocker rows** (UI) — or, on the CLI without `--create-missing-sites` / `--create-missing-connections`, abort before apply.
**FK rewiring at apply.** Inside the single EF transaction the importer resolves or creates the target sites and connections **first**, then upserts instances (forced to `NotDeployed`) and rewires their foreign keys against the resolved targets:
- `InstanceConnectionBinding.DataConnectionId` is resolved via the connection map, with a **Pass-2** that also binds connections which already exist in the target but were not carried in the bundle.
- `InstanceNativeAlarmSourceOverride.ConnectionNameOverride` is rewritten to the **mapped target** connection name.
### Stale-Instance Signaling
There is no explicit stale-mark write. Overwriting a template during import changes its content, which naturally changes the flattened-config hash that `DeploymentService.CompareAsync` computes at query time. The Deployments page already surfaces any instance whose computed hash differs from the deployed snapshot — no new mechanism is required. The import result enumerates affected instances (by computing the hash drift before committing) so Step 4 can display a preview count and Step 5 can link directly to the Deployments page.
There is no explicit stale-mark write. Overwriting a template during import changes its content, which naturally changes the flattened-config hash that `DeploymentService.CompareAsync` computes at query time. The Deployments page already surfaces any instance whose computed hash differs from the deployed snapshot — no new mechanism is required.
As of M8 the import result enumerates affected instances for real (closing the prior `StaleInstanceIds` stub). Before commit, for each **Overwritten** template the importer walks the existing **deployed** target instances of that template and, via the `IStaleInstanceProbe` seam in Commons (implemented in the Deployment Manager over the flattening pipeline), compares each instance's freshly-flattened revision hash against its `DeployedConfigSnapshot.RevisionHash`; instances whose hash has drifted are returned in `ImportResult.StaleInstanceIds`. The probe is optional — when it is not registered the result is an empty set (informational only, never blocking). Freshly-imported `NotDeployed` instances are surfaced as **new**, not stale. Step 5 (Confirm) shows the real count and Step 6 (Result) deep-links the Deployments page filtered to those instances.
## Error Handling
@@ -265,10 +303,12 @@ There is no explicit stale-mark write. Overwriting a template during import chan
| Upload | `bundleFormatVersion` newer than supported | Step 1 error: "Bundle was created by ScadaBridge v{x}; upgrade this cluster" |
| Upload | Content hash mismatch | Step 1 error: "Bundle integrity check failed — file may be corrupt" |
| Unlock | Wrong passphrase | Step 2 error; 3rd wrong attempt invalidates session, audit `BundleImportUnlockFailed` |
| Preview | Bundle references shared script not in bundle and not in target DB | Listed as a blocker row in Step 3; cannot Apply until resolved |
| Map | Source site/connection neither auto-matched nor mapped/created | Listed as a blocker row in the Map step (UI) / Diff step; cannot Apply until mapped or set to Create-new. CLI aborts before apply unless `--create-missing-sites` / `--create-missing-connections` is given |
| Map | `CreateNew` selected but the bundle does not carry that site/connection's config | Blocker — Create-new requires the full config to be present in the bundle payload |
| Preview | Bundle references shared script not in bundle and not in target DB | Listed as a blocker row in the Diff step; cannot Apply until resolved |
| Apply | Semantic validation fails (call target type mismatch, etc.) | Modal: "Validation failed — N errors", per-error list, no DB writes |
| Apply | DB transaction fails | Rollback; full transactional guarantee — nothing partial lands; `BundleImportFailed` audit row written outside the rolled-back transaction |
| Session | TTL expired | Step 3+ refresh prompts re-upload |
| Session | TTL expired | Diff step onward, refresh prompts re-upload |
Imports are all-or-nothing per bundle. A bundle either applies fully or not at all.
@@ -316,17 +356,22 @@ Three commands surface the same Transport operations as the Central UI wizards,
scadabridge bundle export --output FILE --passphrase X [--all | --templates A,B ...] \
[--shared-scripts ...] [--external-systems ...] [--db-connections ...] \
[--notification-lists ...] [--smtp-configs ...] [--api-keys ...] \
[--api-methods ...] [--include-dependencies] [--source-environment NAME]
[--api-methods ...] [--sites A,B ...] [--instances X,Y ...] \
[--include-dependencies] [--source-environment NAME]
scadabridge bundle preview --input FILE --passphrase X
# prints PreviewBundleResult JSON: per-row items + add/modified/identical/blocker counts
# prints PreviewBundleResult JSON: per-row items + add/modified/identical/blocker counts;
# also prints a required-mapping summary — for each source site/connection it states the
# auto-match ("auto-matches 'X'") or the directive ("use --map-site / --create-missing-sites")
scadabridge bundle import --input FILE --passphrase X [--on-conflict skip|overwrite|rename]
scadabridge bundle import --input FILE --passphrase X [--on-conflict skip|overwrite|rename] \
[--map-site src=dst ...] [--map-connection srcSite/srcName=dstName ...] \
[--create-missing-sites] [--create-missing-connections]
# one-shot load + preview + apply with a single global policy for Modified rows
# Identical → Skip, New → Add, Blocker → abort
```
Selection uses entity **names** rather than IDs so scripts are portable across environments. The CLI per-command timeout is 5 minutes (vs the default 30 s for other commands) to comfortably cover large bundles. CLI commands route through `ManagementActor`'s `ExportBundleCommand` / `PreviewBundleCommand` / `ImportBundleCommand` handlers, which delegate to the same `IBundleExporter` / `IBundleImporter` services as the UI.
Selection uses entity **names** rather than IDs so scripts are portable across environments`--sites` accepts a `SiteIdentifier` (preferred) or friendly name per token, `--instances` accepts a `UniqueName`. The mapping flags are repeatable; a `--map-site`/`--map-connection` token with no `=dst` part (or `=` with an empty right-hand side) means **Create-new**, otherwise it binds to the named existing target. `--create-missing-sites` / `--create-missing-connections` create any still-unmapped source site/connection from the bundle payload instead of aborting. The CLI per-command timeout is 5 minutes (vs the default 30 s for other commands) to comfortably cover large bundles. CLI commands route through `ManagementActor`'s `ExportBundleCommand` / `PreviewBundleCommand` / `ImportBundleCommand` handlers, which delegate to the same `IBundleExporter` / `IBundleImporter` services as the UI.
Exit codes follow the project convention: `0` = success, `1` = command failure (validation, blockers, wrong passphrase), `2` = authorization failure.
@@ -342,8 +387,9 @@ Exit codes follow the project convention: `0` = success, `1` = command failure (
## Dependencies
- **`ZB.MOM.WW.ScadaBridge.Commons`** — Bundle manifest and content DTOs (`BundleManifest`, `ExportSelection`, `ImportPreview`, `ImportResolution`, `ImportResult`, `BundleSession`); transport interface definitions (`IBundleExporter`, `IBundleImporter`, `IBundleSessionStore`, `IAuditCorrelationContext`).
- **`ZB.MOM.WW.ScadaBridge.ConfigurationDatabase`** — All repository implementations and `IAuditService` for persistence and per-entity audit emission; `IAuditCorrelationContext` implementation (`AuditCorrelationContext`) registered as a scoped service; EF migration for `BundleImportId`.
- **`ZB.MOM.WW.ScadaBridge.Commons`** — Bundle manifest and content DTOs (`BundleManifest`, `ExportSelection`, `ImportPreview`, `ImportResolution`, `ImportResult`, `BundleSession`); the name-mapping model (`BundleNameMap`, `SiteMapping`, `ConnectionMapping`, `MappingAction`); transport interface definitions (`IBundleExporter`, `IBundleImporter`, `IBundleSessionStore`, `IAuditCorrelationContext`, `IStaleInstanceProbe`).
- **`ZB.MOM.WW.ScadaBridge.ConfigurationDatabase`** — All repository implementations and `IAuditService` for persistence and per-entity audit emission; `IAuditCorrelationContext` implementation (`AuditCorrelationContext`) registered as a scoped service; EF migration for `BundleImportId`. Site/`DataConnection`/`Instance` transport (T18) reuses the **existing** entities — no new tables or columns.
- **`ZB.MOM.WW.ScadaBridge.DeploymentManager`** — `IStaleInstanceProbe` implementation (`StaleInstanceProbe`) over the flattening pipeline, supplying the real `ImportResult.StaleInstanceIds` enumeration (optional DI registration; absent → empty, informational-only result).
- **`ZB.MOM.WW.ScadaBridge.TemplateEngine`** — Pre-deployment `SemanticValidator` invoked inside `ApplyAsync` before the transaction commits. The importer builds a single-template `FlattenedConfiguration` directly from each imported `TemplateDto` (no inheritance / composition resolution at design time — the deployment-time flatten revalidates against the full instance graph) and feeds it through the validator alongside a `ResolvedScript` catalog combining in-bundle + pre-existing target `SharedScript`s. Validator errors are aggregated per template and surfaced as a `SemanticValidationException` that rolls back the import transaction.
## Interactions
@@ -363,7 +409,7 @@ The `manifest.json` file is always present in the ZIP root and is never encrypte
```json
{
"bundleFormatVersion": 1,
"schemaVersion": "1.0",
"schemaVersion": "1.1",
"createdAtUtc": "2026-05-24T12:34:56Z",
"sourceEnvironment": "dev-cluster-a",
"exportedBy": "alice@corp.example",
@@ -385,7 +431,10 @@ The `manifest.json` file is always present in the ZIP root and is never encrypte
"notificationLists": 1,
"smtpConfigs": 0,
"apiKeys": 2,
"apiMethods": 5
"apiMethods": 5,
"sites": 2,
"dataConnections": 3,
"instances": 8
},
"contents": [
{
@@ -399,6 +448,24 @@ The `manifest.json` file is always present in the ZIP root and is never encrypte
"name": "Pump.WaterPump",
"version": 3,
"dependsOn": ["Template:Pump", "ExternalSystem:HistorianAPI"]
},
{
"type": "Site",
"name": "site-a",
"version": 1,
"dependsOn": []
},
{
"type": "DataConnection",
"name": "site-a/PlantOpcUa",
"version": 1,
"dependsOn": ["Site:site-a"]
},
{
"type": "Instance",
"name": "WaterPump-01",
"version": 1,
"dependsOn": ["Template:Pump.WaterPump", "Site:site-a"]
}
]
}
@@ -408,8 +475,8 @@ The `manifest.json` file is always present in the ZIP root and is never encrypte
| Field | Description |
|---|---|
| `bundleFormatVersion` | Integer. Importer hard-refuses any value higher than what its `TransportOptions.SchemaVersionMajor` supports. |
| `schemaVersion` | Semver string. Minor increments are additive-only and accepted by older importers. |
| `bundleFormatVersion` | Integer. Importer hard-refuses any value higher than what its `TransportOptions.SchemaVersionMajor` supports. Stays `1` through T18. |
| `schemaVersion` | Semver string. Minor increments are additive-only and accepted by older importers. T18 bumped this `1.0 → 1.1` (adding the `sites`/`dataConnections`/`instances` content arrays); a pre-T18 importer lists those new entries as "skipped — unsupported in this version" since `ManifestValidator` gates only on `bundleFormatVersion`. |
| `createdAtUtc` | ISO-8601 UTC timestamp of when the export was created. |
| `sourceEnvironment` | The `SourceEnvironment` name of the exporting cluster (from `TransportOptions`). Displayed in the import wizard and required to be retyped at the confirm step. |
| `exportedBy` | Authenticated username of the person who performed the export. |
@@ -421,5 +488,5 @@ The `manifest.json` file is always present in the ZIP root and is never encrypte
| `encryption.iterations` | PBKDF2 iteration count. Defaults to 600 000 (configurable via `TransportOptions.Pbkdf2Iterations`). |
| `encryption.saltB64` | Base64-encoded 16-byte random salt generated at export time. |
| `encryption.ivB64` | Base64-encoded 12-byte (GCM standard) random IV generated at export time. |
| `summary` | Artifact count by type, for display in the import wizard's upload step without needing to decrypt content. |
| `contents` | Ordered list of all artifacts in the bundle. Order is topological (base templates before derived). Each entry carries the artifact's name, the schema `version` at export time, and its direct `dependsOn` edges for dependency display in the export wizard's Step 2. |
| `summary` | Artifact count by type, for display in the import wizard's upload step without needing to decrypt content. Schema 1.1 adds `sites` / `dataConnections` / `instances` counts. |
| `contents` | Ordered list of all artifacts in the bundle. Order is topological (base templates before derived; sites before their connections and instances). Each entry carries the artifact's name (connections use the site-qualified `{SiteIdentifier}/{Name}`), the schema `version` at export time, and its direct `dependsOn` edges for dependency display in the export wizard's Step 2. T18 adds the `Site`, `DataConnection`, and `Instance` entry `type`s. |
+17 -5
View File
@@ -1667,8 +1667,10 @@ scadabridge --url <url> api-method delete --id <int>
Export, preview, and import Transport (#24) bundles. Bundles carry templates, shared
scripts, external systems, database connections, notification lists, SMTP configurations,
and API methods between environments. Inbound API keys are **not** transported — recreate
them on the destination via the CLI or UI.
and API methods between environments. As of M8 (T18) they also carry **site-scoped**
configuration — `Site` definitions, site-scoped `DataConnection`s (protocol connections),
and `Instance`s (with their override children and area membership). Inbound API keys are
**not** transported — recreate them on the destination via the CLI or UI.
Bundle commands use a 5-minute timeout (larger payloads may be slow over WAN).
@@ -1693,6 +1695,8 @@ scadabridge --url <url> bundle export --output <path> [--passphrase <string>] [-
| `--notification-lists` | no | Comma-separated notification-list names to include |
| `--smtp-configs` | no | Comma-separated SMTP host names to include |
| `--api-methods` | no | Comma-separated API-method names to include |
| `--sites` | no | Comma-separated site identifiers (preferred) or friendly names to include |
| `--instances` | no | Comma-separated instance unique-names to include (also pulls their site + bound data connections) |
| `--source-environment` | no | `SourceEnvironment` value stamped into the bundle manifest (default: `cli`) |
**Example** — export two templates and all their dependencies:
@@ -1704,7 +1708,7 @@ scadabridge --url <url> bundle export --output baseline.scadabundle \
#### `bundle preview`
Load a bundle and print the diff preview (Added / Modified / Unchanged per entity) without applying any changes.
Load a bundle and print the diff preview (Added / Modified / Unchanged per entity) without applying any changes. When the bundle carries sites or site-scoped connections, the preview also prints a **required-mapping summary**: for each source site/connection it states either the auto-match (`auto-matches 'X'`) or the directive to supply `--map-site` / `--map-connection` or `--create-missing-*`.
```sh
scadabridge --url <url> bundle preview --input <path> [--passphrase <string>]
@@ -1717,10 +1721,12 @@ scadabridge --url <url> bundle preview --input <path> [--passphrase <string>]
#### `bundle import`
Load and apply a bundle with a single global conflict policy. Preview first with `bundle preview` to review the diff.
Load and apply a bundle with a single global conflict policy. Preview first with `bundle preview` to review the diff and the required-mapping summary.
```sh
scadabridge --url <url> bundle import --input <path> [--passphrase <string>] [--on-conflict <policy>]
scadabridge --url <url> bundle import --input <path> [--passphrase <string>] [--on-conflict <policy>] \
[--map-site <src=dst> ...] [--map-connection <srcSite/srcName=dstName> ...] \
[--create-missing-sites] [--create-missing-connections]
```
| Option | Required | Default | Description |
@@ -1728,6 +1734,12 @@ scadabridge --url <url> bundle import --input <path> [--passphrase <string>] [--
| `--input` | yes | — | Bundle file path (`.scadabundle`) |
| `--passphrase` | no | — | Passphrase for encrypted bundles |
| `--on-conflict` | no | `overwrite` | Resolution policy for `Modified` rows: `skip`, `overwrite`, or `rename` |
| `--map-site` | no | — | Map a source site to a destination: `srcIdentifier=dstIdentifier`. A token with no/empty `=dst` means **create-new**. Repeatable. |
| `--map-connection` | no | — | Map a source connection to a destination: `srcSiteIdentifier/srcName=dstName`. A token with no/empty `=dst` means **create-new**. Repeatable. |
| `--create-missing-sites` | no | `false` | Create any still-unmapped/unmatched source site from the bundle payload instead of aborting |
| `--create-missing-connections` | no | `false` | Create any still-unmapped/unmatched source connection from the bundle payload instead of aborting |
Site identifiers and connection names are environment-specific; the importer first auto-matches them (sites by `SiteIdentifier`, connections by the site-qualified `{SiteIdentifier}/{Name}`). Any source reference that neither auto-matches nor is mapped/created aborts the import. Imported instances always land `NotDeployed` — redeploy via the normal flow.
---
+4 -4
View File
@@ -47,7 +47,7 @@ Real, but narrower than the spec — wrong in a way that could surprise an opera
| **13** | **Inbound `Object`/`List` extended types are shape-validated only** — no nested/field-level type validation, despite spec implying typed/nested validation. | `ParameterValidator.cs:109-145`; `ReturnValueValidator.cs:18` |
| **14** | **JWT-in-cookie session design not implemented**`/auth/login` signs a plain `ClaimsPrincipal`; `GenerateToken` only used by the CLI `/auth/token` path; `ValidateToken` has no external callers. | `AuthEndpoints.cs:38,75-112,152`; `ServiceCollectionExtensions.cs:99-118` |
| **15** | **"Re-query LDAP every 15 min / roles never >15 min stale" not implemented for interactive sessions** — `JwtTokenService.RefreshToken`/`RecordActivity`/`ShouldRefresh`/`IsIdleTimedOut` have **zero** call sites; roles fixed until cookie expiry. The 15-min sliding + 30-min idle layers are collapsed into a single 30-min sliding cookie window. | `JwtTokenService.*` (no callers); `ServiceCollectionExtensions.cs:99-148` |
| **16** | **Transport stale-instance enumeration always returns empty**`BundleImporter` returns `Array.Empty<int>()`; UI shows a generic warning with no count, link not filtered to stale instances. | `BundleImporter.cs:733`; `TransportImport.razor:347-388` |
| **16** | **FIXED (M8/T18).** Transport stale-instance enumeration is real: before commit, for each Overwritten template `BundleImporter` enumerates deployed target instances whose freshly-flattened revision hash drifts from `DeployedConfigSnapshot.RevisionHash` via the new `IStaleInstanceProbe` seam (Commons; implemented in DeploymentManager) and returns their ids; Confirm shows the real count and the result deep-links the filtered Deployments page. Was `Array.Empty<int>()`. | `BundleImporter.cs` (`ComputeStaleInstanceIdsAsync`); `IStaleInstanceProbe.cs`; `StaleInstanceProbe.cs`; `TransportImport.razor` |
| **17** | **`MachineDataDb` fail-fast requirement not enforced** — spec (REQ-HOST-3/4) requires central nodes to validate a non-empty `MachineDataDb` connection string. `DatabaseOptions` has only `ConfigurationDb`/`SiteDbPath`; validator never checks it; 0 `grep` hits in `src/`. Key lives only in docker appsettings as dead config. | `DatabaseOptions.cs:6-12`; `StartupValidator.cs:60-61` |
| **18** | **CI grep-guard against `UPDATE/DELETE … AuditLog` not in the repo** — spec claims a build-time grep that fails on data-layer mutations. DB-role DENY enforcement *is* present in migrations (so this is a backstop, not the only control), but the claimed code-level guard is absent. | spec `Component-AuditLog.md:335-336`, `Component-ConfigurationDatabase.md:297` |
@@ -98,9 +98,9 @@ Knowingly punted, with extensible seams and explicit doc notes. `[PERM]` = perma
- `[DELIVERED M7/T17]` OPC UA "Verify endpoint" connectivity button (captures-but-never-trusts an untrusted cert) + site-local cert-management UI (per-node `CertStoreActor`, DeploymentManager broadcast to both nodes). *Follow-up:* central-persisted, auditable cert trust (site-local today).
**Transport (#24)**
- `[PERM]` Site-scoped / instance-scoped artifact transport (needs name-mapping subsystem).
- `[PERM]` Direct cluster-to-cluster pull; asymmetric bundle signing; differential/incremental bundles.
- `[PERM/SLICE]` Per-line/Myers diff for Modified artifacts (coarse line-count delta only). — `ArtifactDiff.cs:18-25`
- `[DELIVERED M8/T18]` Site-scoped / instance-scoped artifact transport with the `BundleNameMap` name-mapping subsystem. Moves `Site`s, site-scoped `DataConnection`s, and `Instance`s (+ override children + `Area` by name); auto-match (sites by `SiteIdentifier`, connections by `{SiteIdentifier}/{Name}`, instances by `UniqueName`) with operator override via the import-wizard Map step / CLI `--map-site`/`--map-connection`/`--create-missing-*`; D3 carry-full-config (Site addresses travel; `DataConnection` config rides the encrypted `SecretsBlock`, presence-only in diffs); schema 1.1 (additive). *Follow-ups:* DeploymentManagerRepository hydration; large-bundle/perf hardening.
- `[DELIVERED M8/T20]` Per-line/Myers diff for Modified code fields — pure `LineDiffer` (custom Myers O(ND), no third-party lib); `ArtifactDiff` embeds a size-capped structured `lineDiff` (hunks + `truncated` + add/remove totals); rendered by the `LineDiffView` import-wizard component.
- `[PERM]` Direct cluster-to-cluster pull; asymmetric bundle signing; differential/incremental bundles. — **deferred** (T19, its own brainstorm).
**TreeView**
- `[SLICE/PERM]` R6 lazy-loading, R7 keyboard nav, R16 multi-select — spec marks all "(Deferred)". — `Component-TreeView.md:87-93,288-295`