Inspection Scope Migration Plan
#Inspection Scope Migration Plan — Use inspections.organizationId/siteId for scope reads
- Request: Org and Site id column usage update in inspection table across usage.
- Why:
inspections.organizationId/inspections.siteIdnow exist and are backfilled, but many read paths still infer scope viainspections.assetId -> assets. - Risk being fixed: incorrect scoping when inspected asset is not the site root (ex: inverter inspections), plus slower query paths due to unnecessary asset-join inference.
Switch inspection-scope read paths to canonical inspection columns:
i."organizationId"i."siteId"
Keep behavior/functionality equivalent for dashboard, ET, compare, reports, BAM, and deep links.
#In Scope
- SQL scope resolvers and inspection-scope selection logic
- Backend org/site access checks that currently depend on
inspection.asset.* - Backend service/job context derivation for org/site from inspection
- Frontend inspection context sync (org/site switch) from inspection payload
- Frontend
Inspectiontype update to include top-levelorganizationIdandsiteId - Tests/fixtures impacted by scoping source change
#Out of Scope (explicit)
- Finding/asset rule lookup behavior
- Work order org checks
- Geospatial/hierarchy asset filtering behavior
- Schema migration + backfill for
inspections.organizationId/inspections.siteId. - Writer paths (create/update/ingest/restore).
- Inspections list query migration.
inspections.organizationIdandinspections.siteIdare now canonical and backfilled.- Some read paths still derive scope through
inspection.assetId -> assetsorinspection.asset.*. - That causes wrong org/site scope in non-site-root inspections (for example inverter inspections), and adds unnecessary join-based inference.
- This migration standardizes scope resolution:
- Scope / access / context:
inspection.organizationId,inspection.siteId - Inspected asset identity:
inspection.assetId
- Scope / access / context:
#1) GET /v1/inspections (including compare=true candidate mode)
- Functions
InspectionsService.getInspectionsInspectionsRepository.findComparableInspections
- Files
backend/src/modules/inspections/inspections.service.tsbackend/src/modules/inspections/inspections.repository.tsbackend/src/modules/inspections/sql/find-comparable-inspections.sql
#2) GET /v1/inspections/findings
- Functions
InspectionsService.listFindingsInspectionsService.getFindingsInspectionsRepository.resolveScopedInspectionIds
- Files
backend/src/modules/inspections/inspections.service.tsbackend/src/modules/inspections/inspections.repository.tsbackend/src/modules/inspections/sql/resolve-scoped-inspection-ids.sql
#3) GET /v1/inspections/:id/findings/summary (legacy + scoped ET mode)
- Functions
InspectionsService.getFindingsSummaryInspectionsRepository.resolveScopedInspectionIds
- Files
backend/src/modules/inspections/inspections.service.tsbackend/src/modules/inspections/inspections.repository.tsbackend/src/modules/inspections/sql/resolve-scoped-inspection-ids.sql
#4) GET /v1/inspections/compare
- Functions
InspectionsService.compareInspectionsInspectionsService.validateInspectionAccessInspectionsRepository.compareInspectionSummaryAggregatesInspectionsRepository.findComparableInspections
- Files
backend/src/modules/inspections/inspections.service.tsbackend/src/modules/inspections/inspections.repository.tsbackend/src/modules/inspections/sql/compare-summary-aggregates.sqlbackend/src/modules/inspections/sql/find-comparable-inspections.sql
#5) GET /v1/inspections/compare/findings
- Functions
InspectionsService.compareInspectionFindingsInspectionsService.validateInspectionAccessInspectionsRepository.compareInspectionFindingsPage
- Files
backend/src/modules/inspections/inspections.service.tsbackend/src/modules/inspections/inspections.repository.tsbackend/src/modules/inspections/sql/compare-findings-page.sql
#6) GET /v1/organizations/:orgId/dashboard/widget/:widgetId
- Functions
DashboardRepository.getChartPowerLossTrend- shared scope fragment consumed by widget repository methods (
getKpiPowerLossPct,getKpiRevenueAtRisk,getKpiFireRisk,getKpiDefectDensity,getChartAnomalyPareto,getRankSites)
- Files
backend/src/modules/dashboard/sql/resolve-dashboard-inspection-scope.sqlbackend/src/modules/dashboard/sql/chart-power-loss-trend.sql
#7) Reports APIs (/v1/inspections/:id/reports*, /v1/reports)
- Functions
ReportsService.validateInspectionAccessReportsService.getAllReportsReportsService.getReportsForInspectionReportsService.getReportByIdReportsService.streamReportFile
- Files
backend/src/modules/reports/reports.service.ts
#8) GET /v1/admin/bam/inspection-pollers
- Functions
AdminBamService.getInspectionPollers
- Files
backend/src/modules/admin-bam/admin-bam.service.ts
#9) POST /v1/ai-workflow/detect-anomaly
- Functions
AiWorkflowService.detectAnomalyDetectAnomalyHandler.handle
- Files
backend/src/modules/ai-workflow/ai-workflow.service.tsbackend/src/modules/jobs/handlers/detect-anomaly.handler.ts
#10) POST /v1/inspections/:id/import-anomalies
- Functions
InspectionsService.buildImportComputeContextInspectionsService.importAnomaliesImportAnomaliesHandler.handleImportAnomaliesHandler.handleDataplaneImport
- Files
backend/src/modules/inspections/inspections.service.tsbackend/src/modules/jobs/handlers/import-anomalies.handler.ts
#11) PATCH /v1/ai-agent/threads/:threadId/context (inspection access path)
- Functions
assertThreadContextAccessassertInspectionAccess
- Files
backend/src/modules/ai-agent/context/context-access.validation.ts
#12) GET /v1/organizations/:orgId/assets/hierarchy (inspection-scoped rollup path)
- Functions
AssetService.validateInspectionInOrganizationAssetRepository.findInspectionOrgScopesiteInspectionSelectForSitesiteInspectionSelectForSiteRootsAssetRepository.getRolledUpFindingsCountsForSiteRoots
- Files
backend/src/modules/asset/asset.service.tsbackend/src/modules/asset/asset.repository.ts
#13) Frontend consumers of the above inspection APIs (context sync)
- Functions / logic points
Inspectioncontract in API client- org/site deep-link synchronization logic in inspection details, AI review, and DT route flows
- Files
frontend/src/services/api.tsfrontend/src/pages/inspections/InspectionDetailsPage.tsxfrontend/src/pages/findings/AIFindingReviewPage.tsxfrontend/src/hooks/digital-twin-v2/useDigitalTwinInspection.tsfrontend/src/pages/digital-twin-v2/DigitalTwinPage.tsx
#Runtime code
- None identified. All required runtime changes are connected to at least one API path in List 1.
#Test files (expected updates)
backend/src/modules/inspections/inspections.repository.spec.tsbackend/src/modules/inspections/inspections.service.spec.tsbackend/src/modules/inspections/inspection-compare.integration.spec.tsbackend/src/modules/inspections/inspections.scope-summary.provided-ids.integration.spec.tsbackend/src/modules/dashboard/dashboard.repository.spec.tsbackend/src/modules/dashboard/dashboard.service.spec.tsbackend/src/modules/dashboard/dashboard.response-contract.spec.tsbackend/src/modules/reports/reports.service.spec.tsbackend/src/modules/admin-bam/admin-bam.service.spec.tsbackend/src/modules/ai-workflow/ai-workflow.service.spec.tsbackend/src/modules/ai-agent/context/context-access.validation.spec.tsbackend/src/modules/asset/asset.repository.spec.tsfrontend/src/services/api.spec.ts- Frontend tests covering:
InspectionDetailsPage.tsxAIFindingReviewPage.tsxuseDigitalTwinInspection.tsDigitalTwinPage.tsx
Implementation appendix. Use the API map above as the primary checklist; keep this section for low-level execution details.
#Current problem
- Uses
i."assetId" AS site_id - Scopes by joining
assetsand filteringa."organizationId"/a.id
#Required change
- Scope inspections directly:
i."organizationId" = $1::text($2::text IS NULL OR i."siteId" = $2::text)
- Emit
site_idfromi."siteId". - Keep findings-existence logic and latest-per-category semantics intact.
#Guardrail
- Do not re-introduce scope dependency on
i."assetId"for site scoping.
#Downstream impact note (important)
- The following dashboard SQL files consume this scope via
-- __DASHBOARD_SCOPE_CTES__and are expected to inherit behavior after resolver fix:kpi-power-loss-pct.sqlkpi-revenue-at-risk.sqlkpi-fire-risk.sqlkpi-defect-density.sqlchart-anomaly-pareto.sqlrank-sites.sql
- These files are verify-only in this migration unless a hidden local scope assumption is discovered.
#Files
backend/src/modules/dashboard/sql/kpi-power-loss-pct.sqlbackend/src/modules/dashboard/sql/kpi-revenue-at-risk.sqlbackend/src/modules/dashboard/sql/kpi-fire-risk.sqlbackend/src/modules/dashboard/sql/kpi-defect-density.sqlbackend/src/modules/dashboard/sql/chart-anomaly-pareto.sqlbackend/src/modules/dashboard/sql/rank-sites.sql
#What to verify
- They continue to use only
resolved_inspections/resolved_sitesfrom shared CTE marker. - They do not directly read
horizongrid.inspections ifor scope. - They do not introduce
i."assetId"site scoping logic.
#What should NOT change
- No direct scope rewrite in these files unless test/data reveals incorrect local assumptions.
- Keep existing metric formulas/filters/order unchanged.
#Current problem
- Scopes inspections with
JOIN inspections i ON i."assetId" = ss.site_id.
#Required change
- Scope using inspection columns:
i."organizationId" = $1- site filter using
i."siteId"
- Ensure partitioning/grouping by site uses
i."siteId". - Preserve existing trend output contract and sort order.
#Current state
- Already filters with
i."organizationId"/i."siteId". - Still joins assets for selected asset subtree filtering.
- Latest partition currently uses
fi."assetId".
#Required change
- Keep asset join only for explicit subtree filter logic (
$3/$4) where needed. - Confirm/adjust latest partition key based on intended semantics:
- if site-scope latest, partition by
fi."siteId"+ category - if asset-scope latest within site, partition by
fi."assetId"+ category
- if site-scope latest, partition by
- Document decision in SQL comment to avoid regressions.
#findInspectionOrgScope
- Replace org/site derivation from joined asset with direct inspection columns.
#siteInspectionSelectForSite
- Replace
i."assetId" = siteIdwithi."siteId" = siteId.
#siteInspectionSelectForSiteRoots
- Replace
i."assetId" = ANY(siteRootIds)withi."siteId" = ANY(siteRootIds). - Latest partition key should be
i."siteId", c.category(site-based selection).
#getRolledUpFindingsCountsForSiteRoots
- In
site_inspectionsCTE, join/filter usingi."siteId"instead ofi."assetId".
#Current problem
- Many call sites source org/site from
inspection.asset.organizationId/siteId.
#Required change
- Replace inspection-scope org/site derivation with top-level inspection columns:
- BAM sync inputs
- findings scope fallbacks
- inverter scope/assignment context
- compare/org-site context mapping where applicable
- Keep
inspection.asset.*only for asset identity/navigation concerns.
#Current problem
validateInspectionAccessdepends oninspection.asset.organizationId.
#Required change
- Select and validate using
inspection.organizationId(andinspection.siteIdwhere needed). - Keep behavior of role/access checks unchanged.
- Preserve report filtering semantics when site filter is provided; descendant asset inclusion behavior must remain equivalent after scope migration.
- Replace
siteId = inspection.assetIdwithinspection.siteId. - Replace org derivation from asset with
inspection.organizationId.
- In
handle(...), update inspection load + scope derivation to use inspection columns for scope:- keep inspection fetch include(s) needed by downstream logic.
- replace:
const siteId = inspection.assetId;const organizationId = inspection.asset.organizationId;
- with:
const siteId = inspection.siteId;const organizationId = inspection.organizationId;
- Add guard for missing
inspection.siteId(should not happen after backfill, but fail fast with clear error for safety). - Ensure
loadSiteAIConfig(...)is called with(organizationId, siteId)from inspection columns only. - Keep
inspection.assetIdusage only if explicitly needed for asset identity (not for scope).
- In compute context setup, replace asset-derived scope:
- current pattern uses
inspection.asset.organizationId/inspection.asset.siteId. - update to
inspection.organizationId/inspection.siteId.
- current pattern uses
- Keep
inspection.assetIdargument passed toprocessDamageGroup(...)as-is where it represents the inspected asset identity for finding writes. - Add explicit inline comment in code:
inspection.siteId/organizationId => scope,inspection.assetId => finding target asset identity. - Add null-site guard before building compute context (error/log + fail job clearly if absent).
- Update
assertInspectionAccess(...)inspection select to include top-level fields:organizationId,siteId, and (only if needed)assetId.
- Perform org ownership check using
inspection.organizationId(remove dependency oninspection.asset.organizationId). - Perform site ownership check using
inspection.siteIdfirst:if (siteId && inspection.siteId !== siteId) -> Forbidden.- keep backward-compatible allowance only if business explicitly requires legacy asset-id-as-site matching; otherwise remove that fallback.
- Call
assertUserSiteAccess(...)withsiteId ?? inspection.siteId. - Retain nested asset select only if another non-scope validation still requires asset metadata; otherwise drop asset include/select entirely to reduce query work.
- Extend
Inspectiontype with:organizationId: stringsiteId?: string | null
- For deep-link org/site sync, use
inspection.organizationId/inspection.siteId.
- Same org/site sync migration as above.
- Derive effective/synced org/site from top-level inspection fields.
- Update inspection route-context checks to top-level fields.
#Optional hardening candidates (not required for current acceptance)
backend/src/modules/inspections/inspection-redis-sync.service.ts- Replace org/site derivation from
inspection.asset.*with inspection top-level columns for scope-sensitive sync payloads.
- Replace org/site derivation from
backend/src/modules/inspections/inspection-image.service.ts- Replace inspection access/context org derivation from
image.inspection.asset.organizationIdwith inspection-level org/site fields where scope is intended.
- Replace inspection access/context org derivation from
backend/src/modules/inspections/inspection.anomaly.service.ts- Replace org/site scope derivation from
inspection.asset.*with inspection-level fields.
- Replace org/site scope derivation from
backend/src/common/pixel-to-asset/module-derivation.cli.ts- For inspection-scope/org-site derivation, prefer
finding.inspection.organizationId/siteIdinstead of nested asset scope fields.
- For inspection-scope/org-site derivation, prefer
#Keep as-is (intentional asset identity usage)
backend/src/modules/inspections/inspections.service.ts(importAnomalypath)inspection.assetIdis used as the finding asset target (CreateFindingDto.assetId), not site scope.
backend/src/modules/jobs/handlers/import-anomalies.handler.tsinspection.assetIdpassed toprocessDamageGroupis the inspected-asset identity for finding writes.
backend/src/modules/jobs/handlers/database-restore.handler.ts- writer/backfill behavior for
assetIdlinkage is not part of read-scope migration.
- writer/backfill behavior for
#Clarification
- Rule of thumb for this migration:
- Use
inspection.organizationId/inspection.siteIdfor scope, access, and context resolution. - Keep
inspection.assetIdfor inspected-asset identity and asset-linked write semantics.
- Use
resolve-scoped-inspection-ids.sqllatest partition key:- Keep
assetId + categoryif ET semantics are per inspected asset. - Switch to
siteId + categoryonly if product confirms site-level latest semantics.
- Keep
find-comparable-inspections.sql:- Keep candidate matching on same
assetId(inspected asset identity). - Move only org/site validation to inspection columns.
- Keep candidate matching on same
- Reports site filter parity:
- Ensure pre/post migration report counts for site-scoped requests are unchanged.
#Phase 1 — SQL Scope Core
- Update inspection SQL scope resolvers (
resolve-scoped-inspection-ids,resolve-dashboard-inspection-scope,chart-power-loss-trend,find-comparable-inspections). - Update corresponding SQL-shape specs.
#Phase 2 — Backend Scope Consumers
- Update
asset.repository.tsscope selectors. - Update
inspections.service.ts,reports.service.ts,admin-bam.service.ts. - Update AI/workflow/job handlers and context validation.
#Phase 3 — Frontend Contracts and Context
- Add
organizationId/siteIdtoInspectiontype. - Migrate context sync logic in details/AI review/digital twin flows.
- Adjust affected UI tests.
#Phase 4 — Validation & Smoke
- Backend tests (targeted + impacted integration).
- Frontend coverage/type-check for touched flows.
- Manual smoke: list / ET / dashboard / compare / reports / deep-link org-site switcher.
#Phase 5 — Optional hardening (only if requested after acceptance)
inspection-redis-sync.service.tsinspection-image.service.tsinspection.anomaly.service.tscommon/pixel-to-asset/module-derivation.cli.ts
#Backend automated
pnpm formatpnpm buildpnpm lintpnpm test
#Frontend automated
pnpm buildpnpm lintpnpm type-checkpnpm test:coverage
#Manual smoke scenarios
- Inspection list still resolves correctly under org and site scopes.
- ET-related inspection scoping behavior unchanged for site + all-sites.
- Dashboard widgets return same/better results for inspections with non-site
assetId. - Compare endpoints (options + summary/findings) still return expected pair sets.
- Reports access checks enforce org/site access correctly.
- Deep-linked inspection pages auto-sync org/site context correctly.
#Acceptance parity checklist (before final sign-off)
- Dashboard parity validated (org/site scopes; non-site
assetIdinspections included correctly) - ET parity validated (scoped ids and latest logic unchanged unless explicitly intended)
- Compare parity validated (candidate sets and ordering remain correct)
- Reports parity validated (site/organization filtering counts unchanged)
- BAM parity validated (poller/sync scope behavior unchanged or improved)
- Site scope uses
i."siteId", noti."assetId"- covered in SQL resolvers + asset repository selectors.
- Org access checks use inspection columns
- covered in reports/AI/context/service updates.
- Dashboard / ET / compare / reports / BAM parity
- covered in phased updates and smoke checklist.
- Tests + frontend updated
- covered in test file inventory + validation plan.
- Plan only. Do not implement until explicit approval: "approved, proceed".