feat/sites-1n
needs attentionviewing older commit23c04fc · incrementalpre-PRreviewed 2026-07-07 20:30 UTC3H · 3M · 2L · 3I- Purpose
- Correct the Site↔RPU 1-to-N relationship: stop fusing unrelated RPUs onto one site, dedup only when internal ID and name both match, and give users UX to see and manage the relationship.
- Goal
- Make the Site↔RPU relationship correct, visible, and manageable — shared site-resolution decision, match-driven Nuevo RPU drawer, editable Site cell with pencil→modal, and group-by-site view.
- Sub-goals
- SG-1: Shared site-resolution decision + rewired coordinators (done)
- SG-2: Site match/search endpoint — extended sites.list with search + contractCount (done)
- SG-3: createFromWizard accepts existingSitePublicId (done)
- SG-4: Nuevo RPU drawer — Crear/Usar-existente segmented with debounced search (done)
- SG-5: Site cell pencil → SiteEditModal: edit/move/create-new (done)
- SG-6: Agrupar por sitio toggle (done)
- SG-7: Batch preview grouping with existing-site flags (done)
- SG-8: 4 analytics events + CLAUDE.md update (done)
- What
- This incremental window (2 commits): org-scope the SUC move candidate in linkSiteToContractShell (uses findSitesForContractInOrg instead of global findByContractId to prevent cross-org SUC hijacking) + correct stale RBAC annotations in sites.contract.ts (admin+ → member+ on create/update/delete/updateLocation — comments now match the handlers).
- Why
- Global findByContractId returned all org SUCs for a contract, so in the shared-resource model (same utility contract linked across multiple orgs) the shell could accidentally move another org's SUC. The RBAC comment corrections reflect the actual handler enforcement that was already in place.
- Areas
- apps/platform+1211−173domains/cross-domain+452−131domains/utility/src/site-utility-contract+61−5domains/core/src/site+33−1packages/api + packages/analytics+63−2docs/development + .branch + .github/workflows+410−108
- Blast
- ~25 files, +2,230/−420 across the branch; this incremental window is narrow (+17/−9, 2 files).
Findings · 12
correctness2
newSite-null guard is safe: decision rejects before existingSuc is inspected
domains/utility/src/site-utility-contract/site-utility-contract.shells.ts:300
When newSite is null, decideLinkSiteToContract returns SiteNotFound before reaching the MoveLinkedSite/FirstClaim branch. Outcome identical to old code. No silent failure introduced.
Ordering improvement: findSitesForContractInOrg adds deterministic ORDER BY
domains/utility/src/site-utility-contract/site-utility-contract.queries.ts:130
Old findByContractId had no ORDER BY — existingLinks[0] was nondeterministic. New query orders by createdAt ascending, making [0] the oldest org-scoped SUC. Correctness improvement.
security2
DELETE site at member-level with no SUC dependency guard enables data orphaning
domains/core/src/site/site.queries.ts:152
getDependencies is a stub returning {assets:0}; no FK on site_utility_contracts.site_id→sites.id. Any org member can delete a site with active SUC rows, silently breaking contract bindings and cascade-deleting monitoring subscriptions. Pre-existing bug amplified by the member-level RBAC change.
No FK on site_utility_contracts.site_id creates a TOCTOU race window
domains/utility/src/site-utility-contract/site-utility-contract.shells.ts:300
A concurrent site deletion between the handler read and shell write would produce a dangling site_id. Pre-existing structural gap made more reachable at member-level. Fix: add FK with ON DELETE RESTRICT or extend getDependencies to count SUCs.
conventions1
Branch name in JSDoc RBAC comments will rot (3 occurrences)
apps/platform/src/api/contracts/sites.contract.ts:52
'— see feat/sites-1n RBAC audit' appears in 3 comment blocks. CLAUDE.md prohibits referencing the current task/fix/branch in comments. Drop the trailing clause; the preceding sentence conveys the policy without the volatile reference.
improvement2
Null-guard short-circuit semantics are implicit
domains/utility/src/site-utility-contract/site-utility-contract.shells.ts:300
contract && newSite guard returns [] when either is null; the decision errors on the missing entity anyway. The fallback to [] never has semantic meaning — a reader must trace the decision to see why it is safe.
updateLocation RBAC comment wording diverges from 3 sibling endpoints
apps/platform/src/api/contracts/sites.contract.ts:230
Uses 'Any member of the organization, checked via site lookup' vs the template 'Any member of the organization (handlers enforce member-level; ...)' used by create/update/delete. Minor inconsistency, no correctness issue.
test-coverage5
No integration test for the cross-org SUC isolation fix in linkSiteToContractShell
domains/utility/src/site-utility-contract/__tests__/site-utility-contract.decisions.test.ts:1
Behavioral change lives in the shell fetch-step, not the decision. Missing scenario: Org A has SUC for contract C; Org B member moves C to a different Org B site — should move only Org B's SUC, leave Org A's untouched. No shells integration test file exists at all.
FirstClaim path for cross-org contract sharing is untested
domains/utility/src/site-utility-contract/__tests__/site-utility-contract.decisions.test.ts:1
When Org A has a SUC for contract C and Org B calls linkSiteToContractShell, the shell routes to FirstClaimLinkedSite (insert new SUC). No test verifies this path — a future global-dedup change would collapse two org claims silently.
resolveVersionServerSide OCC path is untested
domains/utility/src/site-utility-contract/__tests__/site-utility-contract.decisions.test.ts:79
Server-side version resolution now uses the org-scoped existingSuc. No test verifies in-transaction version threading or that a concurrent move surfaces as a 409 via reassignSiteWithVersion.
findSitesForContractInOrg query has no integration test
domains/utility/src/site-utility-contract/site-utility-contract.queries.ts:110
Load-bearing primitive for the cross-org fix. Should verify: (1) returns only org-scoped SUCs when contract has multi-org links; (2) returns [] for no match in org; (3) ORDER BY createdAt makes [0] deterministic.
No shell-level test for null contract/site early-exit path
domains/utility/src/site-utility-contract/site-utility-contract.shells.ts:300
The contract && newSite guard now skips the DB query when either is null. Decision tests cover the not-found errors at the decision layer but no test verifies the shell propagates them end-to-end.
History · 8 commits
- 7b5ff24needs attentionincremental0H · 2M · 3L2026-07-09 00:58
- 6231fe5needs attentionincremental0H · 2M · 4L2026-07-08 22:34
- 8fa38b5needs attentionincremental2H · 4M · 5L2026-07-08 01:45
- 83fd5a0needs attentionincremental3H · 8M · 11L2026-07-08 01:37
- 23c04fcneeds attentionincremental3H · 3M · 2L2026-07-07 20:30current
- e70f9f5blockedincremental6H · 11M · 6L2026-07-07 19:28
- 499b1a5blockedincremental6H · 9M · 5L2026-07-07 18:26
- ff23301needs attentionincremental2H · 6M · 5L2026-07-07 01:05