Skip to content

Fix KQL join pitfalls and adopt lookup for dimension enrichment - #2225

Open
RolandKrummenacher wants to merge 15 commits into
devfrom
fix/kql-join-lookup-best-practices
Open

Fix KQL join pitfalls and adopt lookup for dimension enrichment#2225
RolandKrummenacher wants to merge 15 commits into
devfrom
fix/kql-join-lookup-best-practices

Conversation

@RolandKrummenacher

@RolandKrummenacher RolandKrummenacher commented Aug 2, 2026

Copy link
Copy Markdown
Collaborator

📝 Changes

Follow-up to a repo-wide review of KQL join vs lookup best practices (~880 join/lookup usages examined across hub database scripts, workbooks, ARG recommendation queries, the query catalog, and docs). This PR fixes the high-severity correctness bugs, adopts lookup where it is the documented best practice, and adds a lint rule so bare joins cannot come back. One commit per surface:

  1. Workbooks (SavingsPlan, AHB ×2) — correctness fixes:
    • SavingsPlan summary/details: the final resourcecontainers join had no kind and defaulted to innerunique, deduplicating results by subscription and silently dropping all but one savings plan recommendation per subscription. Now kind=inner.
    • AHB "VM Latest Change Last 7 days": joined the resourcechanges record id against resources.id, which never match, so the tile was always empty. Now joins properties.targetResourceId (lowercased both sides) with kind=inner so mv-expanded license changes are preserved.
    • Get-SQL-AHB-Disabled/Enabled: bare join on VMName dropped SQL VMs with duplicate names (innerunique) and never matched uppercase names (left original case vs right tolower). Now joins properties.virtualMachineResourceId against the VM resource id.
  2. ARG recommendation queries + finops-alerts logic app — every bare | join ( made an explicit kind=inner; Recommendations-Microsoft-SQLVMsWithoutAHB gets the same virtualMachineResourceId join-key fix as above. docs/deploy/finops-alerts-*.json are generated and intentionally untouched; the next release build picks up the bicep change.
  3. Hub ingestion scripts — the v1_0/v1_2 EA transforms enriched cost rows from the open-data dimension tables (PricingUnits, Regions, ResourceTypes, Services) with join kind=leftouter; converted to lookup kind=leftouter (broadcasts the small dimension, no duplicated key columns; no downstream references to the suffixed columns existed). Also guards the Services enrichment against row fan-out with summarize take_any(...) by x_ResourceTypethis was an active bug: Services.csv has 30 duplicate resource-type keys (up to ×31 for microsoft.sql/locations), so cost rows for those types were being multiplied.
  4. Query catalogtagging-policy-compliance, storage-tier-distribution, macc-consumption-vs-commitment: fact-to-small-dimension joins converted to lookup.
  5. Docs — compute.md AHB examples (bare joins + the same VMName case bug, published as copy-paste guidance), commitment-coverage examples now teach lookup, invalid join ... on 1 == 1 percent-of-total examples rewritten with toscalar(), fullouter examples now coalesce their join keys so baseline-only rows keep their dimension values, and networking.md examples fixed (bare joins + an invalid resource table name that made the idle public IP example fail outright).
  6. Lint rule — new Tests/Lint/KqlJoinKinds.Tests.ps1 scans every KQL-carrying surface and fails on any bare | join without an explicit kind=. The 48 remaining pre-existing bare joins (4 workbook files, all with unique left keys today) are baselined per file as a ratchet: counts can only go down (tracked in Workbooks: make remaining implicit and innerunique ARG join kinds explicit #2228). The SQL DB optimization runbook and networking.md were brought to zero in this commit.

✅ Validation

All changes that can run against live services were validated — three rounds documented in the comments:

  • Round 1: ARG innerunique default proven with a controlled probe; Services fan-out confirmed from open data; ingestion lookup chain executed against a hub ADX cluster; old-vs-new catalog queries equivalent on 1.16M cost rows.
  • Round 2: logic app queries, all edited doc examples (old on 1 == 1 versions fail live with General_BadRequest; new toscalar() versions return correct totals), fullouter fixes eliminate the empty-key rows, and all 8 edited transform/HubSetup functions compile-checked against real schemas.
  • Round 3: two production-scale hub deployments (36.6M / 28.3M rows) — the pre-fix Services join is deployed in their live transforms; A/B on populated dimension tables shows 1 row → 5/4/2 rows with the old join vs 1:1 with the new lookup; no existing data corruption found (both ingest via a FOCUS path predating the enrichment). Lint suite: 161/161 passing.

⚠️ Known follow-ups (tracked as issues)

🤖 Generated with Claude Code

Roland Krummenacher and others added 5 commits August 2, 2026 21:05
- SavingsPlan summary/details: the final resourcecontainers join had no
  kind and defaulted to innerunique, deduplicating the left side by
  subscription and silently dropping all but one savings plan
  recommendation per subscription. Now kind=inner.
- AHB "VM Latest Change Last 7 days": joined resourcechanges record id
  against resources id, which never match, so the tile was always empty.
  Now joins on properties.targetResourceId (lowercased both sides) and
  uses kind=inner so mv-expanded license change rows are not collapsed.
- Get-SQL-AHB-Disabled/Enabled: bare join on VMName dropped SQL VMs with
  duplicate names across resource groups/subscriptions (innerunique) and
  never matched VMs with uppercase names (left was original-case name,
  right tolower(name)). Now joins on the SQL VM
  properties.virtualMachineResourceId against the VM resource id with
  kind=inner, and the tag-filter semi-join states kind=inner explicitly.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Azure Resource Graph joins without an explicit kind default to
innerunique, which deduplicates the left side on the join key and can
silently drop rows. Make every bare join an explicit kind=inner in the
hub recommendation queries and the finops-alerts logic app.

Recommendations-Microsoft-SQLVMsWithoutAHB additionally joined SQL VMs
to compute VMs on VMName with mismatched casing (left original case,
right tolower), so VMs with uppercase names never matched, and duplicate
VM names across resource groups collapsed. It now joins the SQL VM
properties.virtualMachineResourceId against the VM resource id.

docs/deploy/finops-alerts-*.json are generated from logicApp.bicep and
intentionally not hand-edited; the next release build picks this up.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The legacy EA transform functions in IngestionSetup_v1_0/v1_2 enriched
cost rows with the open-data dimension tables (PricingUnits, Regions,
ResourceTypes, Services) via join kind=leftouter. That shape is exactly
what the lookup operator is built for: the large fact table stays on the
left, the small dimension table is broadcast, and the duplicated join
key columns (x_PricingUnitDescription1, ResourceLocation1, ...) are not
emitted. No downstream code referenced the suffixed columns, so output
is unchanged aside from dropping them before the final project.

Also guard the Services enrichment against row fan-out: Services is not
unique per x_ResourceType (a resource type can map to multiple consumed
services), so joining its raw projection could duplicate cost rows.
Dedupe with summarize take_any(...) by x_ResourceType, matching the
pattern already used for the x_ConsumedService fallback, and apply the
same dedup to the existing distinct-based lookups in the FOCUS
transforms and HubSetup_v1_2.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…talog

tagging-policy-compliance, storage-tier-distribution, and
macc-consumption-vs-commitment all join a large fact stream to a small,
key-unique aggregate. Switch those joins to lookup so the small side is
broadcast and the duplicated join key columns are not emitted. The
biggest win is tagging-policy-compliance, where the full Costs() row set
was previously the left side of a hash join against the distinct-tags
dimension. Output schemas are unchanged; the suffixed key columns were
never referenced.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- compute.md AHB queries: bare ARG joins defaulted to innerunique; the
  SQL VM example additionally joined on VMName with mismatched casing
  (left original case vs right tolower), so VMs with uppercase names
  never matched and duplicate names across resource groups were
  silently dropped. Joins are now explicit kind=inner and the SQL VM
  example joins properties.virtualMachineResourceId to the VM id.
- compute.md commitment coverage queries: switch the Prices dimension
  join to lookup kind=leftouter, the recommended pattern for enriching
  the large Costs table from a small key-unique aggregate.
- finops-hub-database-guide.md / ftk-database-query.md: "on 1 == 1" is
  not a valid KQL join predicate; rewrite the percent-of-total examples
  with toscalar(), which is also cheaper (no second full-table join).
- cost-spike/service-cost skill references: coalesce the join keys
  after kind=fullouter so baseline-only rows keep their dimension
  values instead of rendering with empty names.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Roland Krummenacher and others added 2 commits August 2, 2026 21:20
@RolandKrummenacher

Copy link
Copy Markdown
Collaborator Author

✅ Validation results (FTK test tenant + ftk-dev ADX cluster)

ARG semantics probe — confirmed ARG bare join defaults to innerunique: a 40-row left side with one distinct key returns 1 row bare vs 40 rows with kind=inner. The bug class this PR fixes is real.

Services open-data fan-out (commit 3) — confirmed against src/open-data/Services.csv: 30 resource types have duplicate rows, e.g. microsoft.apimanagement/service ×4 and microsoft.sql/locations ×31. Before this PR, the v1_0/v1_2 EA ingestion transforms multiplied every cost row for those resource types by that factor. The take_any dedup eliminates this.

Hub ingestion lookup chain (commit 3) — the exact edited fragment (PricingUnits → Regions → ResourceTypes → Services ×2) executed against the ftk-dev Ingestion database: compiles, returns the expected schema, no duplicated key columns, non-key conflicts suffixed identically to join (so downstream projections are unaffected).

Catalog queries (commit 4) — old vs new run back-to-back on ftk-dev Hub DB (1,162,170 Costs() rows):

  • macc-consumption-vs-commitment: byte-identical
  • storage-tier-distribution: identical rows (order differs; query has no order by)
  • tagging-policy-compliance: identical to 9 decimal places (float summation order)

Recommendation queries + AHB/SavingsPlan workbook queries (commits 1-2) — all executed via the ARG REST API with parameters substituted: parse and run cleanly, incl. the 4-join SavingsPlan queries (ARG accepted 4 joins). Old vs new SavingsPlan counts match in this tenant (1 recommendation per subscription, so innerunique happens to coincide — the probe above shows the general case). resourcechanges.properties.targetResourceId verified populated and resource-id-shaped on 376/376 rows, confirming the new VM Latest Change join key; the old key (resourcechanges record id) can never match a resource id.

Not validated: row-level semantics of the SQL VM / public IP / app gateway queries (test tenant has no IaaS resources — all returned 0 rows, syntax-only), and the logic app queries (validated indirectly via identical shapes + bicep build).

🤖 Generated with Claude Code

Roland Krummenacher and others added 3 commits August 2, 2026 22:13
Adds Tests/Lint/KqlJoinKinds.Tests.ps1, which scans every KQL-carrying
surface (hub scripts, query catalog, ARG recommendation queries, ADX
dashboard, finops-alerts logic app, workbooks, optimization engine
runbooks and views, docs-mslearn best-practices examples) and fails on
any bare "| join" without an explicit kind, since the innerunique
default deduplicates the left side and silently drops rows.

Remaining pre-existing bare joins (48 across 4 workbook files, all with
unique left keys today) are baselined per file as a ratchet: counts can
only go down, and lowering is enforced when a file is cleaned up.

Also brings two surfaces to zero so they need no baseline: the SQL DB
optimization runbook (2 bare joins, left side unique per ResourceId, so
kind=inner preserves behavior) and the networking.md doc examples
(2 bare joins in the backendless app gateway and idle public IP
queries, published as copy-paste guidance).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Live validation of the networking.md idle public IP query failed with
DisallowedLogicalTableName: the joined subquery referenced "resource"
instead of "resources", so the published example never ran. Found while
verifying the explicit join kinds added in this PR; the corrected query
now executes against Azure Resource Graph.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@RolandKrummenacher

Copy link
Copy Markdown
Collaborator Author

✅ Validation round 2 — full coverage of everything testable

Completing the earlier validation, every remaining testable change has now been run against live services (ARG REST API on the FTK test tenant; ftk-dev ADX cluster for hub KQL):

ARG (now all executed):

  • finops-alerts logicApp.bicep: both edited queries extracted from the bicep, variables('resourcesTable') substituted, executed — PASS.
  • compute.md Windows-AHB + SQL-AHB examples — PASS.
  • networking.md backendless app gateway + idle public IP examples — the idle-IP query failed on first run and exposed a pre-existing bug: the published example referenced table resource instead of resources (DisallowedLogicalTableName), so it never ran at all. Fixed in a follow-up commit; now PASS.

Hub ADX cluster (old vs new, Hub DB, 1.16M cost rows):

  • compute.md commitment-coverage queries: join→lookup versions return identical row counts (3 / 100).
  • on 1 == 1 rewrites (finops-hub-database-guide.md, ftk-database-query.md): old versions fail with General_BadRequest (confirming the invalid predicate), new toscalar() versions pass with real data (6 / 44 rows).
  • fullouter key-coalesce fixes: same row counts old vs new, but old had 1 (cost-spike) and 3 (service-deep-dive) rows with an empty key column; new versions have 0 — exactly the fix intent.

Transform functions (compile-checked against real schemas):

  • All 6 edited ingestion transforms (ActualCosts/AmortizedCosts/Costs_transform_v1_0/v1_2) extracted from the scripts and executed with | take 0 against the Ingestion DB (raw tables present) — all compile.
  • Both edited HubSetup_v1_2 functions (CommitmentDiscountUsage_v1_2, Costs_v1_2) compile against the Hub DB.

Mirrors: the three edited queries in Compute/AHB.workbook verified byte-identical to the live-tested AHB/AHB.workbook copies.

Remaining untestable in this environment: row-level semantics of the SQL VM / public IP / app gateway ARG queries (test tenant has no IaaS — all validated as executing with 0 rows).

🤖 Generated with Claude Code

@RolandKrummenacher

Copy link
Copy Markdown
Collaborator Author

✅ Validation round 3 — production-scale hub deployments

Additional read-only validation against two production FinOps hub deployments (36.6M and 28.3M cost rows; identities withheld). Key findings:

The Services fan-out fix (commit 3) is directly relevant to deployed hubs:

  • Both hubs have the Services open-data table populated with the same 30 duplicate resource-type keys as src/open-data/Services.csv.
  • Both hubs have the pre-fix join kind=leftouter (Services | ... project ...) deployed in their live ActualCosts/AmortizedCosts_transform_v1_0/v1_2 functions.
  • Live A/B on one hub's populated dimension tables: the old join turns 1 input row into 5 rows (microsoft.security/pricings), 4 rows (microsoft.web/sites), 2 rows (microsoft.storage/storageaccounts); the new lookup+take_any chain returns exactly 1:1 with correct enrichment values.
  • On one hub, 66% of the last 30 days' cost rows (585,710 of 880,341) carry a duplicate-key resource type — the blast radius if the legacy EA path were used.
  • Two keys (microsoft.security/pricings ×5, microsoft.web/sites ×2) have genuinely different value tuples, so they would fan out even through the newer lookup (Services | distinct ...) variant — validating that this PR also converts those distinct-based lookups to take_any ... by x_ResourceType.

No existing data corruption found in either hub: both currently ingest via the FOCUS path, and their deployed Costs_transform_v1_2 predates the Services enrichment entirely (probe: 0 charges with service-variant duplicates in stored data on both hubs). The buggy join is deployed but not currently exercised — this PR defuses it before it is.

Equivalence re-confirmed on production data: storage-tier-distribution old (join) vs new (lookup) on real multi-tier storage data — identical rows, 0 value mismatches.

🤖 Generated with Claude Code

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟡 Human review recommended

The PR changes KQL behavior across multiple production-impacting surfaces (ingestion, recommendations, workbooks) and also introduces linting coverage gaps that should be resolved before relying on the new guardrail.

Pull request overview

This PR addresses correctness and performance pitfalls in KQL usage across the FinOps Toolkit by making join semantics explicit (avoiding ARG/ADX defaults like innerunique) and adopting lookup for small-dimension enrichment where appropriate. It also introduces a lint rule intended to prevent reintroducing bare join operators.

Changes:

  • Fixed workbook and ARG recommendation queries where implicit/bad joins caused dropped rows or empty results, and made join kinds explicit.
  • Converted several ingestion/catalog “fact-to-dimension” enrichments from join to lookup, including deduping Services mappings to prevent fan-out.
  • Added a Pester lint test to detect bare | join usages (missing kind=) with a per-file baseline/ratchet.
File summaries
File Description
src/workbooks/optimization/SavingsPlan/SavingsPlan.workbook Makes the final subscription enrichment join explicit to prevent innerunique row loss.
src/workbooks/optimization/Compute/AHB.workbook Fixes join keys/kinds for AHB tiles and SQL VM enrichment to avoid mismatches and implicit dedupe.
src/workbooks/optimization/AHB/AHB.workbook Mirrors AHB workbook join-key/kind fixes for correctness and consistency.
src/templates/finops-hub/modules/Microsoft.FinOpsHubs/Recommendations/queries/Recommendations-Microsoft-VMsWithoutAHB.json Makes the subscription join explicit (kind=inner) to avoid implicit innerunique.
src/templates/finops-hub/modules/Microsoft.FinOpsHubs/Recommendations/queries/Recommendations-Microsoft-UnattachedPublicIPs.json Makes the PIP enrichment join explicit (kind=inner).
src/templates/finops-hub/modules/Microsoft.FinOpsHubs/Recommendations/queries/Recommendations-Microsoft-SQLVMsWithoutAHB.json Fixes SQLVM↔VM join key (resourceId) and makes join kinds explicit.
src/templates/finops-hub/modules/Microsoft.FinOpsHubs/Recommendations/queries/Recommendations-Microsoft-BackendlessAppGateways.json Makes join kind explicit for backend pool summarization/enrichment.
src/templates/finops-hub/modules/Microsoft.FinOpsHubs/Analytics/scripts/IngestionSetup_v1_2.kql Uses lookup for dimension enrichment and dedupes Services mapping to prevent fan-out.
src/templates/finops-hub/modules/Microsoft.FinOpsHubs/Analytics/scripts/IngestionSetup_v1_0.kql Same lookup + Services dedupe improvements for v1_0 transforms.
src/templates/finops-hub/modules/Microsoft.FinOpsHubs/Analytics/scripts/HubSetup_v1_2.kql Dedupes Services lookup to avoid x_ResourceType fan-out.
src/templates/finops-alerts/modules/logicApp.bicep Makes embedded ARG query join kinds explicit in the logic app definition.
src/templates/claude-plugin/agents/ftk-database-query.md Rewrites percent-of-total example to avoid invalid join on 1 == 1, using toscalar().
src/templates/agent-skills/finops-toolkit/references/service-cost-deep-dive.md Improves fullouter join examples by coalescing join keys and projecting away duplicates.
src/templates/agent-skills/finops-toolkit/references/cost-spike-investigation.md Same fullouter coalesce pattern for baseline/spike comparison example.
src/queries/finops-hub-database-guide.md Rewrites percent-of-total example using toscalar() instead of join on 1 == 1.
src/queries/catalog/tagging-policy-compliance.kql Converts a leftouter enrichment join to lookup for a small dimension.
src/queries/catalog/storage-tier-distribution.kql Converts an inner join to lookup for currency totals enrichment.
src/queries/catalog/macc-consumption-vs-commitment.kql Converts joins to lookup for small, aggregated right sides.
src/powershell/Tests/Lint/KqlJoinKinds.Tests.ps1 Adds a lint test to detect bare `
src/optimization-engine/runbooks/recommendations/Recommend-SqlDbOptimizationsToBlobStorage.ps1 Makes join kind=inner explicit in embedded KQL.
docs-mslearn/toolkit/changelog.md Updates changelog and ms.date to reflect join/lookup fixes.
docs-mslearn/best-practices/networking.md Updates examples to use explicit join kinds and corrects a table name typo.
docs-mslearn/best-practices/compute.md Updates examples to use lookup and fixes SQL VM join guidance to use resource IDs.
Review details

Suppressed comments (1)

src/powershell/Tests/Lint/KqlJoinKinds.Tests.ps1:70

  • If docs-mslearn/toolkit is added to the scan targets, the current baseline needs an entry for the existing bare joins in docs-mslearn/toolkit/workbooks/customize-workbooks.md (currently 2 occurrences of | join () so the ratchet works as intended.
            'src/workbooks/optimization/AHB/AHB.workbook'         = 24
            'src/workbooks/optimization/Compute/AHB.workbook'     = 20
            'src/workbooks/optimization/Networking/Networking.workbook' = 3
            'src/workbooks/governance/workbook.json'              = 1
        }
  • Files reviewed: 23/23 changed files
  • Comments generated: 1
  • Review effort level: Lite

We're testing this review assessment. Please use 👍 or 👎 to tell us if it's correct.

Comment thread src/powershell/Tests/Lint/KqlJoinKinds.Tests.ps1
Review feedback on PR #2225: the lint scan only covered
docs-mslearn/best-practices, leaving other published docs unguarded.
Scan all of docs-mslearn recursively (122 markdown files) and fix the
two bare joins that surfaced in customize-workbooks.md, which teach the
ResourceContainers-to-resources pattern with the implicit innerunique
default.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
RolandKrummenacher pushed a commit that referenced this pull request Aug 3, 2026
Extends the KQL section with a "Joins and lookups" subsection in the
same intent-first format as the string-matching rules: the innerunique
default trap, lookup as the canonical dimension-enrichment form,
mandatory dimension dedup (take_any by key, not distinct), leftanti for
exclusions, fullouter key coalescing, ARG constraints (no lookup, no
hints, 3-join limit), and the KqlJoinKinds.Tests.ps1 lint that enforces
explicit join kinds with a per-file ratchet baseline.

Grounded in the findings and live validation of PR #2225.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… example

Live-probing Azure Resource Graph (all join flavors + lookup) showed the
docs understate what ARG accepts and the repo overstates it:
- supported: inner, innerunique, leftouter, rightouter, fullouter
- rejected with InvalidQuery: lookup, leftsemi, leftanti, rightsemi,
  rightanti, and in/!in with a subquery

The lint now enforces this: workbooks, recommendation queries, and the
alerts logic app fail the build if they use lookup or a semi/anti join
flavor, with an allowlist escape hatch for legitimate Log Analytics
queries inside workbooks.

The probe surfaced a broken published example: the orphaned-snapshots
query in the azure-cost-management skill used join kind=leftanti, which
ARG rejects, and order by on an uncast dynamic column, which ARG also
rejects. Rewritten as leftouter + isempty (the only exclusion form ARG
supports) with tolower on both join keys and toint on the sort column;
verified executing against ARG.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
RolandKrummenacher pushed a commit that referenced this pull request Aug 3, 2026
Review feedback (Copilot): the avoid/prefer table used backslash-escaped
pipes inside inline code, which renders fine but pastes as invalid KQL
from the raw markdown. The snippets are reworded to not need pipes at
all. The KqlJoinKinds.Tests.ps1 references now note the test is added
in PR #2225, since it does not exist on this branch; merge order is
called out in the PR description.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
RolandKrummenacher pushed a commit that referenced this pull request Aug 4, 2026
…cy example note, markdownlint

- Reword the case-insensitivity rule so it no longer claims every plain
  operator is case-insensitive (== and in are not); scope the claim to
  matching operators with the _cs/equality forms as the opt-in.
- Add an explicit note that the legacy 'join ... on 1 == 1' example
  further down is replaced by toscalar() in #2225 and must not be copied.
- Fix MD036 (bold-as-heading) and align the new change log row's table
  pipes with the header (MD060).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
RolandKrummenacher pushed a commit that referenced this pull request Aug 4, 2026
The rule itself already states the pattern is invalid; the note would go
stale the moment #2225 replaces the example it points at.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@flanakin flanakin added this to the v15 milestone Aug 5, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Needs: Review 👀 PR that is ready to be reviewed

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants