Add bounded Flanders thematic analysis
GeoIntel CI / docs-smoke (push) Canceled after 0s
GeoIntel CI / contract-smoke (push) Canceled after 0s

This commit is contained in:
Codex
2026-07-17 18:28:48 +02:00
parent 21103441cf
commit ce597ee0b4
17 changed files with 389 additions and 55 deletions
+3
View File
@@ -972,6 +972,9 @@ function App(): JSX.Element {
onRunOrthophotoAnalysis={mapOrthophotoAnalysis.run}
onSelectOrthophotoProduct={mapOrthophotoAnalysis.setSelectedProductKey}
onClearQualityEvidence={clearQualityEvidenceGeoJson}
onRefreshProjectData={() => (
selectedProjectId ? loadProjectData(selectedProjectId) : Promise.resolve(null)
)}
onOpenAssistant={() => setActiveWorkspace('assistant')}
onOpenExports={() => setActiveWorkspace('exports')}
/>
+100 -37
View File
@@ -1,13 +1,15 @@
import { useEffect, useMemo, useState } from 'react'
import GeoMap from '../GeoMap'
import type { AreaRead, DatasetCreateResponse, DetectionQaResult, MapResultExportRequest, MapViewportState, OrthophotoAcquisitionResult, OrthophotoProductRead, ProjectRead, QaComparisonResult, VectorSelectionBBox, VectorSelectionMetric, VectorSelectionResponse } from '../../types'
import { useMapThemeSelectionInsights } from '../../hooks/useMapThemeSelectionInsights'
import type { AreaRead, DatasetCreateResponse, DetectionQaResult, MapResultExportRequest, MapViewportState, OrthophotoAcquisitionResult, OrthophotoProductRead, ProjectRead, QaComparisonResult, ThematicRasterProductRead, VectorSelectionBBox, VectorSelectionMetric, VectorSelectionResponse } from '../../types'
import { useMapThemeSelectionInsights, type MapThemeQuery } from '../../hooks/useMapThemeSelectionInsights'
import { useThematicRasterProducts } from '../../hooks/useThematicRasterProducts'
import { useTemporalComparison } from '../../hooks/useTemporalComparison'
import { getDatasetDisplayName, getDatasetSourceDisplayName } from '../../lib/datasetDisplay'
import { TemporalTrendChart } from './TemporalTrendChart'
import { terrainImageUrl } from '../../lib/terrainImage'
import { floodHazardImageUrl } from '../../lib/floodHazardImage'
import { thematicRasterImageUrl } from '../../lib/thematicRaster'
import { FLANDERS_WORKSPACE_PROJECT_NAME } from '../../config/primaryFocus'
import {
bboxToInputState,
bboxesEqual,
@@ -548,6 +550,7 @@ interface MapWorkspaceProps {
onRunOrthophotoAnalysis: (bbox: VectorSelectionBBox) => Promise<boolean>
onSelectOrthophotoProduct: (productKey: string) => void
onClearQualityEvidence?: () => void
onRefreshProjectData: () => Promise<unknown>
onOpenAssistant: () => void
onOpenExports: () => void
}
@@ -632,6 +635,7 @@ export function MapWorkspace({
onRunOrthophotoAnalysis,
onSelectOrthophotoProduct,
onClearQualityEvidence,
onRefreshProjectData,
onOpenAssistant,
onOpenExports,
}: MapWorkspaceProps): JSX.Element {
@@ -640,13 +644,21 @@ export function MapWorkspace({
const selectedDataset = availableMapDatasets.find((dataset) => dataset.id === selectedMapDatasetId) ?? null
return themeIdForDataset(selectedDataset) ?? 'buildings'
})
const activeScopeProject = projects.find((project) => project.id === selectedProjectId) ?? null
const activeScopeLabel = activeScopeProject ? operationalScopeProjectLabel(activeScopeProject) : 'Werkgebied'
const flandersScopeSelected = activeScopeProject?.name === FLANDERS_WORKSPACE_PROJECT_NAME
const {
themeInsights,
themeInsightsLoading: themeResultsLoading,
themeInsightsError: themeResultsError,
loadThemeInsights,
clearThemeInsights,
} = useMapThemeSelectionInsights<DataThemeId>(selectedProjectId)
} = useMapThemeSelectionInsights<DataThemeId>(selectedProjectId, onRefreshProjectData)
const {
products: thematicRasterProducts,
loading: thematicRasterProductsLoading,
error: thematicRasterProductsError,
} = useThematicRasterProducts(flandersScopeSelected ? selectedProjectId : null)
const {
temporalComparison,
temporalComparisonLoading,
@@ -747,6 +759,15 @@ export function MapWorkspace({
[availableMapDatasets, regionalScopeSelected, selectedMapAreaId, themeDatasetMap],
)
const activeTheme = DATA_THEMES.find((theme) => theme.id === activeThemeId) ?? DATA_THEMES[0]
const onDemandThematicProductMap = useMemo(
() => flandersScopeSelected
? new Map<DataThemeId, ThematicRasterProductRead>(
thematicRasterProducts.map((product) => [product.theme, product]),
)
: new Map<DataThemeId, ThematicRasterProductRead>(),
[flandersScopeSelected, thematicRasterProducts],
)
const activeOnDemandThematicProduct = onDemandThematicProductMap.get(activeTheme.id) ?? null
const activeThemeMapStyle = DATA_THEME_MAP_STYLES[activeTheme.id]
const analysisOverlayActive = mapContentMode === 'analysis' && analysisLayerAvailable && Boolean(mapFeatureCollection)
const selectedOrthophotoProduct = orthophotoProducts.find((item) => item.key === selectedOrthophotoProductKey) ?? null
@@ -766,6 +787,9 @@ export function MapWorkspace({
const regionalRasterThemeActive = regionalScopeSelected && isPartitionedRaster(activeThemeDataset)
const regionalBathymetryThemeActive = regionalScopeSelected && isPartitionedBathymetry(activeThemeDataset)
const regionalPartitionedThemeActive = regionalRasterThemeActive || regionalBathymetryThemeActive
const onDemandThematicThemeActive = analysisMode === 'current' && Boolean(activeOnDemandThematicProduct)
const regionalOnDemandThematicThemeActive = regionalScopeSelected && onDemandThematicThemeActive
const activeThemeAvailable = Boolean(activeThemeDataset) || onDemandThematicThemeActive
const terrainImageOverlays = useMemo(
() =>
activeTheme.id === 'elevation' && selectedProjectId
@@ -830,8 +854,6 @@ export function MapWorkspace({
: orthophotoImageOverlay ? [orthophotoImageOverlay] : [],
[floodHazardImageOverlays, orthophotoImageOverlay, terrainImageOverlays, thematicRasterImageOverlays],
)
const activeScopeProject = projects.find((project) => project.id === selectedProjectId) ?? null
const activeScopeLabel = activeScopeProject ? operationalScopeProjectLabel(activeScopeProject) : 'Werkgebied'
const municipalityAreaCount = areas.filter((area) => /^Gemeente\s/i.test(area.name)).length
const themeTemporalSeriesMap = useMemo(
() =>
@@ -855,11 +877,12 @@ export function MapWorkspace({
useEffect(() => {
const contextSourceLabel = analysisMode === 'evolution' ? activeTemporalSeriesGroup?.label ?? null
: regionalBathymetryThemeActive ? 'VHA-dwarsprofielen Vlaanderen'
: null
: activeOnDemandThematicProduct?.display_name ?? null
onSetContextSourceLabel(contextSourceLabel)
return () => onSetContextSourceLabel(null)
}, [
activeTemporalSeriesGroup?.label,
activeOnDemandThematicProduct?.display_name,
analysisMode,
onSetContextSourceLabel,
regionalBathymetryThemeActive,
@@ -873,7 +896,9 @@ export function MapWorkspace({
}),
[themeInsights],
)
const activeSelectionResult = themeResults.find((item) => item.theme.id === activeThemeId)?.result
const activeThemeInsight = themeResults.find((item) => item.theme.id === activeThemeId)
const activeResultDataset = activeThemeInsight?.dataset ?? activeThemeDataset
const activeSelectionResult = activeThemeInsight?.result
?? (!regionalPartitionedThemeActive && selectedMapDataset?.id === activeThemeDataset?.id ? mapSelectionResult : null)
const explorerMapFeatureCollection = regionalBathymetryThemeActive
? null
@@ -1066,15 +1091,15 @@ export function MapWorkspace({
if (!activeSelectionResult) {
return
}
if (activeThemeDataset?.dataset_type === 'raster') {
if (activeResultDataset?.dataset_type === 'raster') {
downloadJsonFile(`${activeTheme.id}-analysis.json`, {
project_id: selectedProjectId,
area_id: areaIdForSelection(mapSelectionBbox) ?? null,
area_name: selectedMapArea?.name ?? null,
theme: activeTheme,
dataset_id: activeThemeDataset.id,
dataset_name: activeThemeDataset.name,
source_name: activeThemeDataset.source_name,
dataset_id: activeResultDataset.id,
dataset_name: activeResultDataset.name,
source_name: activeResultDataset.source_name,
result: activeSelectionResult,
})
return
@@ -1116,19 +1141,19 @@ export function MapWorkspace({
name: `${activeTheme.id}-evolution`,
}
: null
: activeSelectionResult && activeThemeDataset
: activeSelectionResult && activeResultDataset
? {
project_id: selectedProjectId,
mode: 'current',
bbox: { ...mapSelectionBbox, crs: 'EPSG:4326' },
dataset_id: activeThemeDataset.id,
dataset_id: activeResultDataset.id,
area_id: areaId,
partitioned: regionalPartitionedThemeActive,
product_key: regionalRasterThemeActive
? String(activeThemeDataset.source_metadata?.['product_key'] ?? '') || undefined
? String(activeResultDataset.source_metadata?.['product_key'] ?? '') || undefined
: undefined,
partition_scope_key: regionalBathymetryThemeActive
? String(activeThemeDataset.source_metadata?.['partition_scope_key'] ?? 'flanders')
? String(activeResultDataset.source_metadata?.['partition_scope_key'] ?? 'flanders')
: undefined,
theme_id: activeTheme.id,
name: `${activeTheme.id}-analysis`,
@@ -1172,12 +1197,17 @@ export function MapWorkspace({
const dataset = analysisMode === 'evolution'
? temporalGroup?.items[temporalGroup.items.length - 1] ?? null
: themeDatasetMap[theme.id]
if (!dataset) {
const onDemandProduct = analysisMode === 'current'
? onDemandThematicProductMap.get(theme.id)
: null
if (!dataset && !onDemandProduct) {
return
}
setActiveThemeId(theme.id)
clearTemporalComparison()
onOpenDatasetInMap(dataset)
if (dataset) {
onOpenDatasetInMap(dataset)
}
}
const setExplorerMode = (mode: 'current' | 'evolution') => {
@@ -1196,24 +1226,35 @@ export function MapWorkspace({
}
const loadAllThemeResults = async (bbox: VectorSelectionBBox, areaId?: string) => {
const availableThemes = DATA_THEMES.flatMap((theme) => {
const availableThemes: Array<MapThemeQuery<DataThemeId>> = []
for (const theme of DATA_THEMES) {
const thematicProduct = analysisMode === 'current'
? onDemandThematicProductMap.get(theme.id)
: null
if (thematicProduct) {
availableThemes.push({
themeId: theme.id,
thematicProductKey: thematicProduct.key,
})
continue
}
const dataset = themeDatasetMap[theme.id]
return dataset
? [{
themeId: theme.id,
dataset,
partitioned: regionalScopeSelected
&& (isPartitionedRaster(dataset) || isPartitionedBathymetry(dataset)),
}]
: []
})
if (dataset) {
availableThemes.push({
themeId: theme.id,
dataset,
partitioned: regionalScopeSelected
&& (isPartitionedRaster(dataset) || isPartitionedBathymetry(dataset)),
})
}
}
await loadThemeInsights(bbox, availableThemes, areaId)
}
const analyzeSelection = async (bbox: VectorSelectionBBox, areaId?: string) => {
setSelectionBbox(bbox)
const tasks: Array<Promise<unknown>> = [loadAllThemeResults(bbox, areaId)]
if (!regionalPartitionedThemeActive) {
if (!regionalPartitionedThemeActive && !onDemandThematicThemeActive) {
tasks.push(onRunMapSelectionExtract(bbox, areaId))
}
if (analysisMode === 'evolution' && earlierDatasetId && laterDatasetId) {
@@ -1374,11 +1415,14 @@ export function MapWorkspace({
<div className="geo-theme-list">
{DATA_THEMES.map((theme) => {
const dataset = themeDatasetMap[theme.id]
const onDemandProduct = onDemandThematicProductMap.get(theme.id)
const partitions = themePartitionMap[theme.id]
const temporalGroups = themeTemporalSeriesMap[theme.id]
const temporalGroup = temporalGroups[0]
const evolutionAvailable = temporalGroups.some((group) => group.items.length >= 2)
const available = Boolean(dataset) && (analysisMode === 'current' || evolutionAvailable)
const available = analysisMode === 'current'
? Boolean(dataset || onDemandProduct)
: Boolean(dataset) && evolutionAvailable
const active = activeThemeId === theme.id
const temporalRange = temporalGroup ? temporalRangeLabel(temporalGroup.items) : null
return (
@@ -1402,13 +1446,15 @@ export function MapWorkspace({
: 'Bron nog niet ingeladen'
: dataset
? datasetAvailabilityLabel(dataset, partitions)
: onDemandProduct
? `${onDemandProduct.native_resolution_m} m · ${onDemandProduct.observation_year} · laad bij selectie`
: 'Bron nog niet ingeladen'}
</small>
</span>
<i>
{analysisMode === 'evolution'
? evolutionAvailable ? 'Tijdreeks' : dataset ? 'Alleen huidig' : 'Ontbreekt'
: dataset ? 'Beschikbaar' : 'Ontbreekt'}
: dataset ? 'Beschikbaar' : onDemandProduct ? 'Op aanvraag' : 'Ontbreekt'}
</i>
</button>
)
@@ -1424,7 +1470,9 @@ export function MapWorkspace({
? activeTemporalSeriesGroup?.label ?? 'Nog geen historische reeks ingeladen'
: regionalBathymetryThemeActive
? 'VHA-dwarsprofielen Vlaanderen'
: activeThemeDataset ? getDatasetDisplayName(activeThemeDataset) : 'Geen databron beschikbaar'}
: activeThemeDataset
? getDatasetDisplayName(activeThemeDataset)
: activeOnDemandThematicProduct?.display_name ?? 'Geen databron beschikbaar'}
</strong>
<small>
{analysisOverlayActive
@@ -1437,9 +1485,17 @@ export function MapWorkspace({
? `${activeThemePartitions.length} gecontroleerde gemeentepartities · selectie wordt ruimtelijk samengevoegd`
: activeThemeDataset
? `${getDatasetSourceDisplayName(activeThemeDataset)} · ${formatDatasetObservation(activeThemeDataset)}`
: activeOnDemandThematicProduct
? `${activeOnDemandThematicProduct.attribution} · wordt alleen voor de gekozen selectie ingeladen`
: activeTheme.description}
</small>
</div>
{thematicRasterProductsLoading && flandersScopeSelected ? (
<p className="geo-data-notice">Beschikbare Vlaamse beleidsbronnen worden gecontroleerd</p>
) : null}
{thematicRasterProductsError && flandersScopeSelected ? (
<p className="error">{thematicRasterProductsError}</p>
) : null}
{analysisMode === 'current' && activeTheme.id === 'flood_hazard' && floodHazardDatasets.length > 0 ? (
<label className="geo-scope-select">
@@ -1537,6 +1593,8 @@ export function MapWorkspace({
? 'Sleep nu een rechthoek op de kaart.'
: regionalRasterThemeActive
? 'Teken een rechthoek; de juiste gemeentelijke rasters worden automatisch gecombineerd.'
: onDemandThematicThemeActive
? 'Teken een rechthoek; officiële Vlaamse rasters worden begrensd opgehaald, bewaard en hergebruikt.'
: regionalBathymetryThemeActive
? 'Teken een rechthoek of analyseer Vlaanderen; alleen overlappende VHA-partities worden samengevoegd.'
: 'Sleep een rechthoek of analyseer het volledige werkgebied.'}
@@ -1546,7 +1604,7 @@ export function MapWorkspace({
<div className="geo-map-actions">
<button
className={bboxSelectionMode ? 'primary-action geo-draw-active' : 'primary-action'}
disabled={!activeThemeDataset || (analysisMode === 'evolution' && activeTemporalSeries.length < 2) || mapSelectionLoading || themeResultsLoading}
disabled={!activeThemeAvailable || (analysisMode === 'evolution' && activeTemporalSeries.length < 2) || mapSelectionLoading || themeResultsLoading}
type="button"
onClick={startBboxSelection}
>
@@ -1554,12 +1612,16 @@ export function MapWorkspace({
</button>
<button
className="secondary-action"
disabled={!activeThemeDataset || regionalRasterThemeActive || (analysisMode === 'evolution' && activeTemporalSeries.length < 2) || !selectedAreaBbox || mapSelectionLoading || themeResultsLoading}
disabled={!activeThemeAvailable || regionalRasterThemeActive || regionalOnDemandThematicThemeActive || (analysisMode === 'evolution' && activeTemporalSeries.length < 2) || !selectedAreaBbox || mapSelectionLoading || themeResultsLoading}
type="button"
title={regionalRasterThemeActive ? 'Teken een begrensde rechthoek voor een regionale rasteranalyse.' : undefined}
title={
regionalRasterThemeActive || regionalOnDemandThematicThemeActive
? 'Teken een begrensde rechthoek voor een regionale rasteranalyse.'
: undefined
}
onClick={() => selectedAreaBbox && void analyzeSelection(selectedAreaBbox, selectedMapArea?.id)}
>
{regionalRasterThemeActive ? 'Selecteer een deelgebied' : 'Volledig werkgebied'}
{regionalRasterThemeActive || regionalOnDemandThematicThemeActive ? 'Selecteer een deelgebied' : 'Volledig werkgebied'}
</button>
<button className="secondary-action" disabled={!mapSelectionBbox} type="button" onClick={clearAreaSelection}>
Wis selectie
@@ -1881,7 +1943,7 @@ export function MapWorkspace({
{analysisMode === 'current' ? (
<div className="geo-result-actions">
<button className="secondary-action" disabled={!activeSelectionResult} type="button" onClick={downloadActiveThemeResult}>
{activeThemeDataset?.dataset_type === 'raster' ? 'Download analyse' : 'Download GeoJSON'}
{activeResultDataset?.dataset_type === 'raster' ? 'Download analyse' : 'Download GeoJSON'}
</button>
<button className="secondary-action" disabled={!activeSelectionResult} type="button" onClick={copyActiveThemeResult}>Kopieer gegevens</button>
</div>
@@ -1928,7 +1990,8 @@ export function MapWorkspace({
? `VHA-dwarsprofielen Vlaanderen · ${activeThemePartitions.length} gemeentepartities`
: activeThemeDataset
? getDatasetDisplayName(activeThemeDataset)
: 'niet beschikbaar'}
: activeOnDemandThematicProduct?.display_name
?? 'niet beschikbaar'}
</span>
{usesDefaultOsmBasemap ? <span><strong>Ondergrond:</strong> OpenStreetMap</span> : null}
</footer>
@@ -8,16 +8,22 @@ import { thematicRasterSelectionToMapSelection } from '../lib/thematicRaster'
export interface MapThemeQuery<TThemeId extends string> {
themeId: TThemeId
dataset: DatasetCreateResponse
dataset?: DatasetCreateResponse
partitioned?: boolean
thematicProductKey?: string
}
export interface MapThemeInsight<TThemeId extends string> extends MapThemeQuery<TThemeId> {
export interface MapThemeInsight<TThemeId extends string> {
themeId: TThemeId
dataset: DatasetCreateResponse
partitioned?: boolean
thematicProductKey?: string
result: VectorSelectionResponse
}
export function useMapThemeSelectionInsights<TThemeId extends string>(
selectedProjectId: string | null,
onDatasetsChanged?: () => Promise<unknown>,
) {
const [themeInsights, setThemeInsights] = useState<Array<MapThemeInsight<TThemeId>>>([])
const [themeInsightsLoading, setThemeInsightsLoading] = useState(false)
@@ -55,10 +61,32 @@ export function useMapThemeSelectionInsights<TThemeId extends string>(
setThemeInsightsError(null)
try {
const settled = await Promise.allSettled(
queries.map(async ({ themeId, dataset, partitioned }) => ({
themeId,
dataset,
result: dataset.dataset_type === 'raster' && dataset.source_name === 'digitaal_vlaanderen_dhmv'
queries.map(async ({ themeId, dataset: existingDataset, partitioned, thematicProductKey }) => {
let dataset = existingDataset
if (thematicProductKey) {
const acquisition = await datasetsApi.acquireThematicRaster(selectedProjectId, {
bbox,
area_id: areaId,
product_key: thematicProductKey,
force_refresh: false,
})
if (acquisition.status !== 'success' || !acquisition.output_dataset_id) {
throw new Error(
acquisition.error_message
|| `De officiële rasterbron ${thematicProductKey} kon niet worden ingeladen.`,
)
}
dataset = await datasetsApi.get(selectedProjectId, acquisition.output_dataset_id)
}
if (!dataset) {
throw new Error(`Geen persistente databron beschikbaar voor thema ${themeId}.`)
}
return {
themeId,
dataset,
partitioned,
thematicProductKey,
result: dataset.dataset_type === 'raster' && dataset.source_name === 'digitaal_vlaanderen_dhmv'
? terrainSelectionToMapSelection(
partitioned
? await datasetsApi.selectTerrainPartitions(selectedProjectId, {
@@ -100,13 +128,18 @@ export function useMapThemeSelectionInsights<TThemeId extends string>(
area_id: areaId,
limit: 1000,
}),
})),
}
}),
)
const successful = settled.flatMap((item) => (item.status === 'fulfilled' ? [item.value] : []))
const failures = settled.flatMap((item, index) => (
item.status === 'rejected'
? [{
dataset: queries[index]?.dataset.name ?? queries[index]?.themeId ?? 'Onbekende bron',
dataset:
queries[index]?.dataset?.name
?? queries[index]?.thematicProductKey
?? queries[index]?.themeId
?? 'Onbekende bron',
reason: formatError(item.reason, 'Bron kon niet worden bevraagd.'),
}]
: []
@@ -116,6 +149,14 @@ export function useMapThemeSelectionInsights<TThemeId extends string>(
return []
}
setThemeInsights(successful)
let refreshFailure: string | null = null
if (successful.some((item) => item.thematicProductKey) && onDatasetsChanged) {
try {
await onDatasetsChanged()
} catch (error) {
refreshFailure = formatError(error, 'De datasetlijst kon na de analyse niet worden vernieuwd.')
}
}
if (failureCount > 0) {
const details = failures
.slice(0, 4)
@@ -125,6 +166,8 @@ export function useMapThemeSelectionInsights<TThemeId extends string>(
setThemeInsightsError(
`${failureCount} beschikbare databron${failureCount === 1 ? '' : 'nen'} kon niet worden bevraagd. ${details}${remainder}`,
)
} else if (refreshFailure) {
setThemeInsightsError(refreshFailure)
}
return successful
} catch (error) {
@@ -0,0 +1,48 @@
import { useEffect, useState } from 'react'
import { datasetsApi } from '../services/api'
import { formatError } from '../lib/formatError'
import type { ThematicRasterProductRead } from '../types'
export function useThematicRasterProducts(selectedProjectId: string | null) {
const [products, setProducts] = useState<ThematicRasterProductRead[]>([])
const [loading, setLoading] = useState(false)
const [error, setError] = useState<string | null>(null)
useEffect(() => {
let cancelled = false
if (!selectedProjectId) {
setProducts([])
setLoading(false)
setError(null)
return () => {
cancelled = true
}
}
setLoading(true)
setError(null)
void datasetsApi.listThematicRasterProducts(selectedProjectId)
.then((response) => {
if (!cancelled) {
setProducts(response.items)
}
})
.catch((requestError) => {
if (!cancelled) {
setProducts([])
setError(formatError(requestError, 'De Vlaamse beleidsrasters konden niet worden geladen.'))
}
})
.finally(() => {
if (!cancelled) {
setLoading(false)
}
})
return () => {
cancelled = true
}
}, [selectedProjectId])
return { products, loading, error }
}
+2
View File
@@ -64,6 +64,8 @@ async function listProjectDatasets(projectId: string): Promise<DatasetListRespon
export const datasetsApi = {
list: listProjectDatasets,
get: (projectId: string, datasetId: string): Promise<DatasetCreateResponse> =>
apiGet<DatasetCreateResponse>(`/api/v1/projects/${projectId}/datasets/${datasetId}`),
sourceFreshness: (projectId: string): Promise<SourceFreshnessReport> =>
apiGet<SourceFreshnessReport>(`/api/v1/projects/${projectId}/datasets/source-freshness`),
sourceCatalogProbes: (projectId: string, refresh = false): Promise<SourceCatalogProbeReport> =>