AI-generated design. This solution pattern is AI-authored: platform facts cite sources, but the composition, data model, DDL, and code are original design inference and have not passed Fabric Codex human verification.

Scenario#

An investment management firm running portfolio accounting, risk, and performance measurement wants one analytics platform on Microsoft Fabric that ingests three genuinely different source shapes without forcing them through one pipeline pattern: flat files from custodians/administrators, an operational portfolio/transactions database that is the system of record, and market-data/risk-engine APIs polled on a schedule. It wants dbt-modelled gold facts available to Power BI with near-real-time freshness, metadata-driven orchestration instead of one pipeline per entity, and operational telemetry kept separate from business data.

Original diagram: the investment analytics medallion on Fabric — three ingestion paths feed one lakehouse's bronze+silver zones, dbt builds warehouse gold, and Direct Lake serves Power BI, with telemetry and metadata rails separated below

Why each Fabric item was chosen#

  • Data Factory drives the file and API paths: it is Fabric's data-integration workload, connecting to 170+ sources across cloud, multicloud, and on-premises systems via gateways, and chains movement, transformation, and control-flow (loops, conditionals) in one low-code pipeline [S1]. Copy activity is one of three documented movement options alongside Copy job and Mirroring [S1].
  • Mirroring replaces a pipeline for the operational database: it is a fully managed, serverless replication service bringing data from external operational databases into OneLake as Delta Parquet with no ETL to build [S2] — Data Factory's own third movement option, keeping a near-real-time replica so analytics run against the replica instead of the source [S1].
  • One lakehouse (lh_analytics) for bronze and silver is a deliberate simplification: a lakehouse merges lake-scale storage with warehouse-style querying, splitting into a managed Tables area for Delta and a Files area for the rest [S3], and accepts Spark notebooks, pipeline copy, Spark jobs, Dataflows Gen2, or shortcuts [S3] — every route this design needs. Bronze and silver are schemas in the same item rather than two lakehouses (amendment discussed below).
  • Fabric Warehouse (wh_gold) hosts gold because it is an enterprise-scale relational warehouse for star/snowflake schemas and governed BI, with full multi-table ACID transactions on a large T-SQL surface [S4] — a guarantee the lakehouse's read-only SQL analytics endpoint does not give [S3] [S4]. Warehouse tables are still Delta on OneLake [S4], so gold stays one physical copy.
  • dbt (dbt-fabric adapter) builds gold: Fabric Warehouse is a supported dbt target through the community dbt-fabric PyPI package via a profiles.yml type: fabric output [S5]. Fabric documents pairing dbt with Apache Airflow inside Data Factory for scheduled runs [S5], which is why Airflow drives the gold build here.
  • Eventhouse (eh_ops) takes granular operational telemetry: it is purpose-built for high-volume, arrival-ordered event data queryable through KQL at any scale [S6], separate from the relational run audit.
  • Direct Lake / Power BI consumes gold: Direct Lake loads column data from OneLake lazily, reading only the columns a query and its relationships/measures need [S7].

Datasets#

  • Investmentsportfolios, instruments (security master), positions (holdings by portfolio/instrument/date), transactions (buys, sells, corporate actions, cash flows).
  • Riskvar_results (parametric/historical/Monte Carlo VaR), exposures (net/gross by asset class, sector, currency), sensitivities (duration, convexity, greeks, factor betas), stress_results (scenario P&L).
  • Performancereturns_daily, twr (time-weighted return), benchmark_returns, attribution (Brinson-style allocation/selection/interaction).

Inference: these column groupings are original design inference to make the prototype runnable — no Fabric claim specifies an investment data model.

Working prototype scaffold#

sqldb_meta — the control-plane database#

sqldb_meta is a Fabric SQL database [S8]: a developer-oriented transactional engine on the same SQL Database Engine as Azure SQL Database, the designated OLTP home in Fabric [S8]. It automatically replicates into OneLake as Delta/Parquet with a companion SQL analytics endpoint [S8], so the control tables themselves are auditable via T-SQL with no separate reporting path. Authentication is Entra ID only, requiring at least Read item permission [S8].

sql
CREATE TABLE dbo.etl_entity (
    entity_id        INT IDENTITY(1,1) PRIMARY KEY,
    entity_name      NVARCHAR(128)   NOT NULL,   -- 'positions', 'instruments'...
    source_type      VARCHAR(20)     NOT NULL,   -- 'FILE' | 'MIRROR' | 'API'
    source_system    NVARCHAR(128)   NOT NULL,
    load_type        VARCHAR(20)     NOT NULL,   -- 'FULL' | 'INCREMENTAL' | 'CDC_MIRROR'
    target_schema    NVARCHAR(64)    NOT NULL,   -- bronze schema in lh_analytics
    target_table     NVARCHAR(128)   NOT NULL,
    source_path_or_endpoint NVARCHAR(1024) NULL, -- file path or REST URL template
    watermark_column NVARCHAR(128)   NULL,
    schedule_cron    VARCHAR(64)     NOT NULL,
    is_active        BIT             NOT NULL DEFAULT 1,
    created_at       DATETIME2       NOT NULL DEFAULT SYSUTCDATETIME(),
    updated_at       DATETIME2       NOT NULL DEFAULT SYSUTCDATETIME()
);

CREATE TABLE dbo.etl_watermark (
    watermark_id     INT IDENTITY(1,1) PRIMARY KEY,
    entity_id        INT NOT NULL REFERENCES dbo.etl_entity(entity_id),
    watermark_value  NVARCHAR(256)   NOT NULL,
    watermark_type   VARCHAR(20)     NOT NULL,   -- 'DATETIME' | 'INTEGER' | 'STRING'
    updated_at       DATETIME2       NOT NULL DEFAULT SYSUTCDATETIME()
);

CREATE TABLE dbo.etl_run (
    run_id           BIGINT IDENTITY(1,1) PRIMARY KEY,
    pipeline_name    NVARCHAR(128)   NOT NULL,   -- 'pl_ingest_files' | 'pl_ingest_api' | 'pl_gold_airflow'
    triggered_by     VARCHAR(20)     NOT NULL,   -- 'SCHEDULE' | 'MANUAL' | 'RETRY'
    run_status       VARCHAR(20)     NOT NULL,   -- 'RUNNING' | 'SUCCEEDED' | 'FAILED' | 'PARTIAL'
    started_at       DATETIME2       NOT NULL,
    finished_at      DATETIME2       NULL,
    rows_processed   BIGINT          NULL,
    error_summary    NVARCHAR(2000)  NULL
);

CREATE TABLE dbo.etl_run_step (
    step_id          BIGINT IDENTITY(1,1) PRIMARY KEY,
    run_id           BIGINT NOT NULL REFERENCES dbo.etl_run(run_id),
    entity_id        INT NOT NULL REFERENCES dbo.etl_entity(entity_id),
    step_status      VARCHAR(20)     NOT NULL,
    rows_read        BIGINT          NULL,
    rows_written     BIGINT          NULL,
    watermark_before NVARCHAR(256)   NULL,
    watermark_after  NVARCHAR(256)   NULL,
    started_at       DATETIME2       NOT NULL,
    finished_at      DATETIME2       NULL,
    error_message    NVARCHAR(4000)  NULL
);

etl_entity is the registry every pipeline reads at run start; etl_watermark holds the per-entity cursor; etl_run/etl_run_step are the audit trail — the "separate lane" for run-level telemetry described later. Mirrored entities still get an etl_entity row (source_type = 'MIRROR') for inventory only; no pipeline moves their data.

Metadata-driven pipeline pattern — file path#

One pipeline, pl_ingest_files, drives every file entity from etl_entity instead of one pipeline per source: a Lookup activity selects active source_type = 'FILE' rows; a ForEach (non-sequential) iterates them; per item, a Lookup reads the current watermark, a Copy activity moves new files into lh_analytics bronze, and a stored procedure writes the etl_run_step row and advances the watermark. Illustrative parameter shape (not a literal ADF export):

json
{
  "name": "Copy_Bronze_File",
  "type": "Copy",
  "typeProperties": {
    "source": {
      "type": "DelimitedTextSource",
      "storeSettings": { "type": "AzureBlobFSReadSettings", "recursive": true,
        "modifiedDatetimeStart": "@item().watermark_after" }
    },
    "sink": { "type": "LakehouseTableSink", "tableActionOption": "Append" }
  },
  "inputs": [{ "referenceName": "ds_source_file_param",
    "parameters": { "container": "@item().source_path_or_endpoint", "entityName": "@item().entity_name" } }],
  "outputs": [{ "referenceName": "ds_lakehouse_bronze_param",
    "parameters": { "schema": "@item().target_schema", "table": "@item().target_table" } }]
}

This is Copy activity used as Microsoft's recommended mechanism for high-volume recurring ingestion, fanned out at the orchestration layer via a ForEach loop [S1].

Metadata-driven pipeline pattern — API/REST path#

A second pipeline, pl_ingest_api, drives market-data and risk-engine pulls from the same registry filtered to source_type = 'API', with the same Lookup -> ForEach -> Copy -> stored-procedure shape, but a REST source instead of a file source:

json
{
  "name": "Pull_MarketData_REST",
  "type": "Copy",
  "typeProperties": {
    "source": {
      "type": "RestSource", "httpRequestTimeout": "00:05:00", "requestMethod": "GET",
      "additionalHeaders": { "Authorization": "Bearer @{linkedService().apiKey}" },
      "paginationRules": { "supportRFC5988": "true" }
    },
    "sink": { "type": "LakehouseTableSink", "tableActionOption": "Append" }
  },
  "inputs": [{ "referenceName": "ds_rest_source_param",
    "parameters": { "endpointUrl": "@concat(item().source_path_or_endpoint, '?asOf=', item().watermark_after)" } }],
  "outputs": [{ "referenceName": "ds_lakehouse_bronze_param",
    "parameters": { "schema": "@item().target_schema", "table": "@item().target_table" } }]
}

Auth is via a Data Factory linked service, never hardcoded. Both pipelines trigger on etl_entity.schedule_cron and write to the same etl_run/etl_run_step tables, so one query answers "what ran, when, how much, did it fail" across every entity regardless of path.

Mirroring setup — the operational portfolio/transactions database#

The portfolio/transactions system of record is connected via database mirroring, not a pipeline. Mirroring is a fully managed, serverless service replicating external operational databases into OneLake as Delta Parquet with no ETL to build [S2]. Supported database mirroring sources include several well-known engines (Azure SQL Database and Managed Instance, SQL Server, PostgreSQL, and others) [S2]check current supported-source docs for the specific source engine, since the list evolves. Under optimal conditions changes propagate into OneLake in as little as 15 seconds, because the replicator polls a landing zone at high frequency and merges incremental Delta files immediately [S2]; background replication compute is free and does not consume capacity units [S2].

Because mirrored data lands as ordinary Delta tables, lh_analytics does not re-ingest it — it references the mirrored tables via a OneLake shortcut, an object pointing from a shortcut path to a target path elsewhere in OneLake, behaving like a symbolic link (deleting the shortcut leaves the target untouched) [S9]. Internal shortcuts can target other Fabric items, including mirrored databases, across workspaces, authorized with the calling user's own identity, which must hold read permission at the target [S9]. When a shortcut target holds Delta Parquet data, the lakehouse automatically registers it as a table [S9], so mirrored portfolios/transactions appear under lh_analytics/Tables/bronze_mirror/ with no copy step.

Inference: register the mirrored database in etl_entity with source_type = 'MIRROR' for inventory only — no pipeline fires for it, and silver reads whatever the shortcut currently shows.

Bronze layout in lh_analytics#

Append-only, organized by source:

sql
lh_analytics/Tables/
  bronze_files/   positions/  transactions/  instruments/
  bronze_api/     market_data/  risk_engine_results/
  bronze_mirror/  portfolios/  transactions_opdb/   -- shortcut, not a copy

Every bronze table carries load-metadata columns: _ingested_at (UTC write time), _source_file (nullable, file-path entities), _watermark (nullable, the batch's watermark value; null for mirrored/shortcut tables), and _run_id (FK to sqldb_meta.dbo.etl_run.run_id). All managed Tables-area data in a lakehouse is Delta, auto-registered on write with no manual CREATE TABLE [S3]; bronze itself is append-only, so a row's presence is a faithful arrival record.

Silver — PySpark conform notebook (sketch)#

Silver lives in the same lakehouse under a silver/ schema. Sketch for positions:

python
from pyspark.sql import functions as F
from pyspark.sql.window import Window
from delta.tables import DeltaTable

bronze_files = spark.read.table("lh_analytics.bronze_files.positions")

# 1. Dedupe on business key, keep latest ingest
biz_key = ["portfolio_id", "instrument_id", "position_date"]
window = Window.partitionBy(*biz_key).orderBy(F.col("_ingested_at").desc())
deduped = (bronze_files.withColumn("_rn", F.row_number().over(window))
           .filter("_rn = 1").drop("_rn"))

# 2. Enforce schema
conformed = (deduped
    .withColumn("quantity", F.col("quantity").cast("decimal(28,8)"))
    .withColumn("market_value", F.col("market_value").cast("decimal(28,8)"))
    .withColumn("position_date", F.to_date("position_date"))
    .select(*biz_key, "quantity", "market_value", "currency", "_ingested_at", "_source_file", "_run_id"))

# 3. Validate: reject rows missing a business key
valid = conformed.filter(
    F.col("portfolio_id").isNotNull() & F.col("instrument_id").isNotNull() & F.col("position_date").isNotNull())

# 4. Merge into conformed Delta target
target = DeltaTable.forName(spark, "lh_analytics.silver.positions")
(target.alias("t").merge(valid.alias("s"), " AND ".join([f"t.{k} = s.{k}" for k in biz_key]))
    .whenMatchedUpdateAll().whenNotMatchedInsertAll().execute())

The same read -> dedupe -> enforce -> validate -> merge pattern repeats for transactions (key source_system, transaction_id), instruments (instrument_id), portfolios (portfolio_id, merging the mirrored shortcut instead of a file source), returns_daily (portfolio_id, return_date), and risk_measures (portfolio_id, measure_type, as_of_date). Delta's ACID transactions and schema enforcement over Parquet [S3] are what make a repeated MERGE into the same silver table safe to re-run.

Inference: bronze and silver in this design are write-heavy (continuous append/merge), so leave V-Order — a write-time Parquet layout optimization that speeds downstream reads at a typical ~15% write-time cost [S10] — at its default disabled setting here [S10], and consider enabling it only once data reaches gold, where dbt's read-heavy BI queries would benefit most.

dbt project targeting wh_gold#

Fabric Warehouse is a dbt target through the community dbt-fabric adapter (pip install dbt-fabric), not a Fabric-native integration [S5], connecting via a profiles.yml type: fabric output against the SQL analytics endpoint host [S5]. Entra ID auth only — interactive az login for development, service principal for unattended production [S5].

graphql
investment_gold/
  dbt_project.yml
  profiles.yml
  models/
    staging/    sources.yml, stg_positions.sql, stg_transactions.sql, stg_instruments.sql,
                stg_portfolios.sql, stg_returns_daily.sql, stg_risk_measures.sql
    marts/
      dims/     dim_portfolio.sql, dim_instrument.sql, dim_date.sql
      facts/    fact_transactions.sql (incremental), fact_positions_daily.sql (incremental),
                fact_returns_daily.sql, fact_risk_measures.sql
      schema.yml

sources.yml points at silver via three-part names, since cross-database T-SQL can combine Fabric items with zero data duplication [S4] [S8]:

yaml
version: 2
sources:
  - name: lh_analytics_silver
    database: lh_analytics
    schema: silver
    tables:
      - name: positions
      - name: transactions
      - name: instruments
      - name: portfolios
      - name: returns_daily
      - name: risk_measures

Incremental fact, fact_positions_daily.sql:

sql
{{ config(materialized='incremental', unique_key=['portfolio_id','instrument_id','position_date'],
          incremental_strategy='merge') }}

select p.portfolio_id, d.instrument_key, p.position_date, p.quantity, p.market_value, p.currency
from {{ ref('stg_positions') }} p
left join {{ ref('dim_instrument') }} d on p.instrument_id = d.instrument_id
{% if is_incremental() %}
where p.position_date > (select max(position_date) from {{ this }})
{% endif %}

fact_transactions follows the same shape keyed on (source_system, transaction_id). fact_returns_daily/fact_risk_measures can start full-refresh given their smaller daily grain, promoted to incremental if volume grows — inference, not a dbt-fabric recommendation. schema.yml (illustrative):

yaml
version: 2
models:
  - name: fact_positions_daily
    columns:
      - name: portfolio_id
        tests: [not_null]
      - name: instrument_key
        tests: [not_null]
    tests:
      - dbt_utils.unique_combination_of_columns:
          combination_of_columns: [portfolio_id, instrument_key, position_date]

profiles.yml shape:

yaml
investment_gold:
  target: prod
  outputs:
    prod:
      type: fabric
      driver: "ODBC Driver 18 for SQL Server"
      server: "<workspace>.datawarehouse.fabric.microsoft.com"
      database: wh_gold
      schema: dbo
      authentication: ServicePrincipal
      tenant_id: "{{ env_var('FABRIC_TENANT_ID') }}"
      client_id: "{{ env_var('FABRIC_SP_CLIENT_ID') }}"
      client_secret: "{{ env_var('FABRIC_SP_SECRET') }}"

Not every T-SQL operation dbt issues has a direct Fabric equivalent: the adapter implements ALTER TABLE ADD/ALTER/DROP COLUMN, MERGE, TRUNCATE, and sp_rename by translating them into CTAS-plus-DROP/CREATE, because the warehouse's T-SQL surface does not support the full SQL Server command set [S5].

Orchestration runs as an Apache Airflow job inside Data Factory: Fabric documents pairing dbt with Airflow's DAG scheduling rather than a native dbt scheduler [S5], and Data Factory's Airflow integration lets teams express workflows as Python DAGs instead of the visual designer [S1]:

python
from airflow import DAG
from airflow.operators.bash import BashOperator
from datetime import datetime

with DAG(dag_id="gold_dbt_build", schedule_interval="0 6 * * *",
         start_date=datetime(2026, 1, 1), catchup=False) as dag:
    dbt_run = BashOperator(task_id="dbt_run",
        bash_command="dbt run --project-dir /opt/investment_gold --profiles-dir /opt/investment_gold")
    dbt_test = BashOperator(task_id="dbt_test",
        bash_command="dbt test --project-dir /opt/investment_gold --profiles-dir /opt/investment_gold")
    dbt_run >> dbt_test

The DAG's own status is written back to sqldb_meta.dbo.etl_run (pipeline_name = 'pl_gold_airflow') via a final task, keeping run-level audit uniform across file, API, and gold-build lanes.

Original diagram: dbt lineage and the wh_gold star schema for investment risk analytics — stg_positions/stg_returns staging views over lh_analytics silver Delta sources, through dbt dimension models (dim_date, dim_portfolio, dim_benchmark, and SCD Type 2 dim_instrument) and incremental fact models (fact_positions, fact_returns_daily, fact_risk_measures, fact_transactions), to Direct Lake/Power BI

The gold model is a conventional star: fact_transactions, fact_positions_daily, fact_returns_daily, and fact_risk_measures each join dim_portfolio, dim_instrument, and dim_date; fact_risk_measures carries a measure_type degenerate dimension (VaR/exposure/sensitivity/stress) rather than a fourth join table. Original design inference, not a sourced claim.

Telemetry: Eventhouse vs sqldb_meta — the decision rule#

  • eh_ops (Eventhouse) takes high-volume, granular per-event telemetry from Azure Functions — one event per API call attempt, retry, or validation failure. An Eventhouse automatically organizes ingested records by arrival time for fast indexed queries at any scale [S6], which fits bursty function logs far better than a table tuned for a handful of run rows a day. Two integration paths are both viable and worth naming honestly rather than guessing: an Eventstream custom endpoint, where the Function posts to an Eventstream that routes into the Eventhouse with built-in filtering/windowed aggregation first [S11]; or direct Kusto ingestion, where the Function writes straight to the KQL database, lower-latency but skipping Eventstream's in-flight transforms. Either lands a native KQL table queryable via KQL or the database's T-SQL-compatible surface [S6], and if OneLake availability is enabled it becomes directly queryable by Direct Lake, Warehouse, or Spark with no data movement [S6].
  • sqldb_meta.dbo.etl_run/etl_run_step takes run-level, low-cardinality audit — one row per run, one row per entity per run — written transactionally by the pipelines/DAG themselves, and joined via T-SQL against etl_entity/etl_watermark for "why didn't entity X's watermark advance."

Inference: the dividing line is cardinality and query shape — "how many events of type Y happened between 14:02 and 14:03" is Eventhouse; "did today's positions load succeed" is sqldb_meta. This follows from Eventhouse being built for arrival-ordered event-scale data [S6] versus sqldb_meta being a transactional engine meant for control-plane bookkeeping [S8], not a documented rule itself.

Security and workspace layout#

One workspace per medallion stage plus a shared platform workspace, rather than per-domain workspaces:

  • ws-platform-opssqldb_meta, eh_ops, Data Factory pipelines, Airflow job (cross-cutting control-plane, not domain data).
  • ws-bronze-silverlh_analytics and the mirrored database item.
  • ws-goldwh_gold, the dbt-built star schema, and the Direct Lake semantic model.

Justification (inference): per-stage access matches how permissions naturally differ — engineers writing raw/conformed data are a different population from analysts who only read wh_gold's curated facts, and workspace roles are Fabric's grant unit. A per-domain layout (investments/risk/performance workspaces) would isolate blast radius within a domain but triplicate every stage's access rules. Internal shortcuts carry the calling user's own identity and require read permission at the target regardless of workspace [S9], so gold consumers get read-only access scoped to ws-gold without touching bronze/silver — this does not block per-domain row-level restrictions later (e.g. RLS in wh_gold limiting an analyst to their book).

Design review and amendments#

Why gold lives in the warehouse, not the lakehouse. dbt-fabric targets the warehouse's T-SQL surface specifically, compensating for T-SQL gaps (ALTER, MERGE, TRUNCATE) via CTAS/DROP/CREATE [S5]. The warehouse also gives gold full multi-table ACID transactions [S4] — unlike the lakehouse's read-only SQL analytics endpoint — which matters for a dbt MERGE-heavy incremental build competing with concurrent reads. Because warehouse tables are Delta on OneLake and cross-database T-SQL combines Fabric items with zero duplication [S4], wh_gold's models read lh_analytics silver directly via three-part names — no separate physical copy feeds gold, only a T-SQL read across two items in one OneLake.

Lakehouse-only alternative, and why it was not chosen. An honest alternative is to skip the warehouse and build gold as Spark/SQL-endpoint Delta tables inside lh_analytics using dbt-spark instead of dbt-fabric, keeping everything in one item. It was not chosen because dbt's incremental-merge pattern is most battle-tested against dbt-fabric's documented, Microsoft-published path [S5], and the warehouse's transactional T-SQL suits many concurrent incremental MERGEs better than a lakehouse SQL analytics endpoint that cannot accept writes at all [S3]. The cost is a second Fabric item and a second SQL surface purely to get transactional writes; teams fully committed to Spark, or wanting fewer items over dbt convenience, could reasonably choose the lakehouse-only path instead.

Merging bronze and silver into one lakehouse — the trade-off, stated plainly. Per the user's explicit simplification from an earlier two-lakehouse design, bronze and silver are schemas within one lh_analytics item. This buys real simplicity: one set of shortcuts into the mirrored database instead of two, one workspace boundary instead of two, one SQL analytics endpoint/metadata-sync overhead instead of two. It costs blast-radius isolation: a lakehouse's Tables area is one shared namespace, so a runaway Spark job, an accidental DROP TABLE, or an over-broad workspace-role grant that would previously have been scoped to "the bronze lakehouse only" can now reach silver tables too — access control degrades from item-level (separate lakehouse grants) to schema/folder-naming convention (bronze_* vs silver) rather than a Fabric-enforced boundary. This is a deliberate simplicity-over-isolation trade, appropriate for a single team running its own pipeline end-to-end, worth revisiting if bronze and silver ever need materially different access populations or diverging retention/compliance requirements.

Everything downstream of gold stays as designed. Direct Lake reading wh_gold's Delta tables straight from OneLake, lazily loading only touched columns [S7], is unaffected by either amendment — it reads the warehouse's Delta output regardless of how bronze/silver are organized upstream.

Internals#

Architecture & design#

The blueprint composes Fabric ingestion, Delta lakehouse tables, Warehouse modelling, Direct Lake consumption, and operational telemetry. The component boundary is an authored pattern grounded in the cited capability behavior [S1] [S2].

How it works internally#

Source-shaped records land before conformance and history rules publish reusable silver entities; gold models then serve stable analytical contracts. Exact transformation logic remains implementation-specific and must be tested against the declared grain [S1] [S2].

Performance characteristics#

Coming soon. No universal benchmark is inferred for this workload. Measure file layout, transformation duration, Warehouse query behavior, and semantic-model latency against the deployed capacity and data shape.