Skip to content

Refactor sitemap CSV import around a canonical resolved plan (#767)#808

Open
flesher wants to merge 23 commits into
mainfrom
issue-767
Open

Refactor sitemap CSV import around a canonical resolved plan (#767)#808
flesher wants to merge 23 commits into
mainfrom
issue-767

Conversation

@flesher

@flesher flesher commented Jul 23, 2026

Copy link
Copy Markdown
Contributor

Reviewable diff: +2135/-1594 across 6 files (excludes generated, test, and story files).

Summary

Site-map CSV import lets an operator upload a spreadsheet of sites, buildings, racks, and miners and have Proto Fleet reconcile the fleet's topology to match it. Previously each stage of that flow — the dry-run preview, the validation errors, the "did anything change since you previewed" token, and the final database writes — worked out from scratch what the CSV meant. Those four independent re-derivations drifted apart, which is where most of the reported edge-case bugs came from.

This PR makes the import resolve the CSV into one canonical plan first, and then has preview, validation, the commit token, and apply all read from that single plan. Same feature, same CSV format from the user's point of view; the internals now have a single source of truth instead of four.

How it works

The import runs in one direction: raw CSV → resolved plan → everything else.

  1. Parse. The uploaded bytes are split into the four sections (SITE, BUILDING, RACK, MINER), each a list of rows.

  2. Load a snapshot. The server reads what currently exists in the database: all sites, buildings, and racks for the org, plus the miners the uploader is allowed to see. Miners the uploader can't see but which still occupy a rack slot are kept in a separate "hidden members" list so they're never silently trampled or deleted.

  3. Resolve references. Every parent link in the CSV is a single cell, read like this:

    • blank → unassigned
    • a bare number (e.g. building = 12) → the existing building with id 12
    • NAME:x (e.g. building = NAME:Warehouse B) → a building being created in this same import whose name is "Warehouse B"

    This replaces the old scheme where each link was two columns (a name and an id) that could disagree. Resolving also fills in implied ancestors (a rack that names a building inherits that building's site) and records the matched building's real id internally, so two different buildings that happen to share the same (site, name) stay distinct.

  4. Build the plan. Each row becomes a resolved node (site / building / rack / miner) tagged with an action — none, create, or update — decided by comparing the canonical CSV row against the live row field by field. Alongside the nodes, a "topology view" describes the desired end state (CSV merged over the current snapshot) that the validators check against.

  5. Validate, count, tokenize — all off the plan. Grid-position collisions, rack and building capacity, slot conflicts and bounds, and rename permissions all read the resolved nodes and the shared topology view (and fold in those hidden members). The preview change-counts come from the node actions. The commit token is a hash of the resolved plan plus the live snapshot fingerprint.

  6. Apply. On commit, the token is re-checked (if the CSV or the live database changed since the dry-run, the token won't match and the commit is refused). Then a single database transaction walks the resolved nodes and performs each create/update, and — because "remove omitted rows" is now the only removal mode — deletes anything the CSV left out.

Example of the token guard: you dry-run and get a token. A teammate moves a rack in the UI before you hit commit. Your token no longer matches the fresh snapshot, so the commit is rejected and you're asked to dry-run again, rather than applying a stale plan.

flowchart TD
    CSV["Uploaded CSV"] --> Parse["Parse into SITE/BUILDING/RACK/MINER rows"]
    DB["Live database"] --> Snap["Load snapshot (org topology + visible miners + hidden members)"]
    Parse --> Resolve["Resolve references (single-cell: blank / id / NAME:x)"]
    Snap --> Resolve
    Resolve --> Plan["Resolved plan: nodes tagged none/create/update + topology view"]
    Plan --> Preview["Preview change counts"]
    Plan --> Validate["Validation (grid, capacity, slots, renames)"]
    Plan --> Token["Commit token = hash(plan + snapshot)"]
    Plan --> Apply["Apply: one transaction, walk nodes, delete omitted"]
    Apply --> DB
Loading
sequenceDiagram
    actor User
    participant Import as Import service
    participant DB
    User->>Import: Upload CSV (dry_run)
    Import->>DB: Load snapshot
    Import->>Import: Resolve plan, validate, count
    Import-->>User: Preview + commit token
    User->>Import: Commit (with token)
    Import->>DB: Reload snapshot
    Import->>Import: Recompute token
    alt token matches
        Import->>DB: Apply plan in one transaction
        Import-->>User: Success + changes
    else snapshot drifted
        Import-->>User: Rejected — dry-run again
    end
Loading

Areas of the code involved

Area / package / file What changed Why it matters for review
server/internal/domain/sitemap/resolved.go New. The canonical plan: resolved node types, resolveReferences, resolvePlan, the topology view, action classification, and all validators that now read the plan. This is the heart of the PR — the single source of truth everything else consumes.
server/internal/domain/sitemap/service.go Rewritten around the plan: export is id-authoritative, preview/token/apply read the plan, and the old per-stage re-derivation helpers are deleted (net ~-900 lines). Confirm preview, token, and apply all derive from the plan and that the deleted helpers have no remaining callers.
server/internal/domain/buildings/models/models.go New helpers GridCapacity and RackCapacityExceeded — the building rack-capacity rule extracted so both the buildings domain and sitemap import share one implementation. A previously duplicated invariant; check the two callers agree on semantics.
server/internal/domain/buildings/service.go Uses the extracted capacity helper instead of its own inline arithmetic. Behavior-preserving; verify the net-membership count passed in is unchanged.
client/.../SiteMapCsvImportModal.tsx Removes the "leave omitted rows in place" radio option; the omission prompt now just confirms removal. Matches the server: remove-omitted is the only committable behavior.
docs/plans/2026-07-22-767-sitemap-canonical-plan-tdd.md New design/plan document. Optional context for the refactor's phasing and out-of-scope items.

Key technical decisions & trade-offs

  • Single reference cell (id or NAME:) instead of a name+id column pair. The old pair could contradict itself and needed reconciliation logic; one cell removes that whole class of ambiguity. Trade-off: existing entities must be referenced by id, and same-import creations by NAME: — bare names are a validation error.
  • Id-precise building identity via an internal companion cell. Two buildings can legitimately share a (site, name) pair, so the resolved building id is carried alongside the canonical name to keep them distinct in grid/capacity/move checks. Alternative — keying purely on (site, name) — silently merged them.
  • Remove-omitted is the only removal mode. "Leave in place" was dropped as a committable option (server and client). Simpler contract; the dead OMISSION_MODE_LEAVE_IN_PLACE proto enum value is left for a follow-up because proto regen is unreliable in this worktree.
  • Commit token hashes the resolved plan, not the raw rows + preview summary. A token can no longer certify a topology that apply would resolve differently. Trade-off: token values change (they're opaque to clients, so this is safe).
  • Extracted the rack-capacity invariant into the buildings domain rather than keeping sitemap's private copy, so the rule has one home. Kept the signature capacity-agnostic (aisles, racksPerAisle, resultingCount) so a future interactive-reparent fix (Rack reparent: over-capacity false rejection when a Manage-racks confirm both removes and reparents into a full building #778) can reuse it.

Testing & validation

  • go build ./..., go vet, and gofmt -l clean; the full internal/domain/sitemap package test suite passes, as do the new buildings/models capacity tests.
  • Behavior is asserted against the resolved plan so the previously-reported edge cases can't silently regress, including: renamed/moved buildings staying visible to the rack grid/capacity validators; id-disambiguation of duplicate (site, name) buildings; hidden rack members counted in slot/capacity/conflict checks; and remove-omitted never unassigning a hidden miner.
  • Commit-token behavior is covered both ways: the token changes when the live snapshot drifts, and when a CSV edit changes the resolved plan even against an identical snapshot.
  • Not covered: deletion of the now-dead OMISSION_MODE_LEAVE_IN_PLACE proto enum value (deferred — needs proto regen outside a worktree); no new end-to-end/integration test beyond the existing service-level suite.

flesher and others added 18 commits July 22, 2026 14:41
Add resolved.go with typed resolvedSite/Building/Rack/Miner nodes and
resolvePlan(), which collapses parsed rows and the live snapshot into one
graph keyed by stable ID with parents linked as pointers. Identity, parent
resolution, ID/name consistency errors, action classification, omission
counts, and the scoped miner population are computed once here.

Introduced unwired: no existing consumer reads the graph yet, so behavior is
unchanged. Later steps route preview counts, validation, apply, and the
commit token through it and remove the per-row desired*/count*/normalize*
helpers it replaces.

Includes the TDD plan doc.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…/7, #767)

buildPlan now builds the resolved plan and consumes it for omission counts
and change summaries via computeChanges. Topology creates come from node
action classification and deletes from omission counts (the same identity
math), so the create/delete/omission counting no longer re-derives the
desired set independently. Update and miner rename/move counts still use the
row-comparison helpers; those migrate when miners are resolved in a later step.

Removes the now-dead countCreates/countSiteCreates/countBuildingCreates/
countRackCreates/countDeletes and the rowSetFromSites/Racks/RackLabels
helpers they used. Change-summary output is unchanged, verified by the
existing suite.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…/7, #767)

Add buildingmodels.GridCapacity and RackCapacityExceeded as the single home
for the aisles × racks_per_aisle rack cap. The buildings-domain guards
(AssignRacksToBuilding, the shrinking UpdateBuilding layout check) and
sitemap's validateBuildingRackCapacity now share this invariant instead of
each recomputing the arithmetic.

RackCapacityExceeded takes a net-final resultingCount and is agnostic about
how it was derived (import graph vs live reassignment), so the interactive
reparent flow (#778) can reuse the same implementation rather than keeping a
third copy. Behavior is unchanged; verified by the sitemap and buildings
suites plus new buildingmodels unit tests.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…plan (#767)

resolvePlan now resolves MINER rows into resolvedMiner nodes matched to their
live miner by device identifier, with rename detection. validateKnownMiners,
validateReadOnlyMinerFields, and the miner-rename change count read these
nodes instead of each rebuilding minerMap(snap.miners) from raw rows.

First group of the validator-to-graph migration. Behavior unchanged, verified
by the existing suite plus a new resolved-miner test. Placement counting and
the remaining topology validators migrate in later groups.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…#767)

validateSlotCollisions now reads resolved miner nodes instead of raw MINER
rows, sharing the same rack-slot fields the rest of the miner validation
already reads. Behavior unchanged.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…logy view (#767)

resolvePlan now builds a topologyView: the desired end-state topology (CSV rows
merged over the omission-target snapshot) that placement-target validators
resolve references against, built once rather than re-derived per validator.
Rack and miner nodes carry the raw site/building references and a building_id
flag so validateBuildingSiteTargets, validateRackPlacementTargets, and
validatePlacementConsistency read the graph instead of rebuilding desired*
maps from raw rows.

Reference resolution is now correctly omission-aware: under remove-omitted the
target view drops omitted topology, matching the prior targetSnap-based calls.
Action classification and prevName still read the full snapshot. Behavior
verified by the existing suite plus a new mode-parity test.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…rough the graph (#767)

validateRackGridPositions and validateRackSlotBounds now read resolved rack and
miner nodes plus the shared topologyView (building layout maps and rack
dimensions) instead of rebuilding desiredBuildingMap / desiredBuildingLayoutIDMap
/ desiredRackMap per call. Rack nodes now carry the parsed building_id reference
for the grid-building lookup. Behavior unchanged.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…e plan (#767)

validateRackCapacity and validateExistingSlotsFitRackDimensions now take the
resolved plan: incoming miners come from resolved.miners, retained/hidden
miners from resolved.population, rack dimensions from topology.racksByLabel, and
rack-rename mapping from the resolved racks' id->label. This drops their
per-call desiredRackMap / desiredRackLabelsByID rebuilds. Behavior unchanged.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…hrough the plan (#767)

validateBuildingRackCapacity, validateBuildingExistingRacksFitLayout, and
validateSlotConflictsWithExisting now take the resolved plan. Building capacity
lookups read topology.buildingsByCapacityKey; existing-rack layout fit reads
the omission-scoped target racks plus the topology building maps; slot conflicts
read resolved.miners and resolved.population with id->label rack rename mapping.
The resolved plan now carries the omission-scoped target snapshot for
existing-topology reconciliation. validateRackGridCollisions stays row-based: it
reconciles against the full (non-omission-scoped) snapshot, which the
target-scoped topology view does not model.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…the plan (#767)

Miner placement changes are now resolved once: classifyMinerMoves sets each
existing miner's moved flag from its desired site/building/rack/slot/placement-id
versus live state. The preview MOVE count reads that flag (countMinerMoveNodes)
and validateMinerRenamePermission reads the renamed flag, both replacing
row-based recomputation. buildPlan exposes the resolved plan on importPlan so
ImportSiteMapCsv can run the rename-permission check against it. Miner nodes now
carry the raw *_id cells needed for placement-identity comparison.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…behavior

Removes the leave-omitted-in-place option so the importer has a single
end-state semantic: omitted topology is removed on commit. Any non-
REMOVE_OMITTED mode carrying omissions now requires explicit confirmation
instead of silently keeping omitted rows, so leave-in-place is no longer
reachable.

- server: the omission confirmation gate (both the request handler and
  buildPlan) keys on != REMOVE_OMITTED rather than == UNSPECIFIED.
- client: drop the "leave omitted rows in place" radio; the omission step
  is now a single confirm-removal choice.
- tests: the leave-in-place mode is replaced by UNSPECIFIED (identical
  full-snapshot topology semantics for the validator fixtures).

The OMISSION_MODE_LEAVE_IN_PLACE proto enum value is now dead; deleting it
from the schema + regenerating is a follow-up (needs the pinned proto
toolchain).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…nces (#767)

Export now always emits the most-specific parent id (building_id, else
site_id; rack_id for miners, else building_id/site_id) and leaves the
parent-reference name columns blank. Existing entities are identified by
stable id rather than by name, which removes the ambiguity-driven
id-vs-name export heuristics (ambiguousBuildingLabels and the
rackExport*/minerExport* helpers).

Change detection stays consistent because normalizeIDReferences back-fills
the parent-name columns from ids before rows are compared, so the
canonical comparable rows keep names populated. Consolidated the redundant
rackRows/rackComparableRow helpers and dropped now-dead code.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ell (#767)

Replace the (*_id, name) column pair for every parent relationship with one
reference cell: blank = unassigned, a bare integer = an existing entity by id,
"NAME:x" = a same-import create named x, anything else = a validation error.

resolveReferences (replacing normalizeIDReferences + normalizeInferredPlacement)
canonicalizes each cell in place to a canonical name with implied ancestors
filled (a building reference also fills its site), so resolve/validate/apply
read names exclusively and never re-interpret an id. This deletes the
id/name-mismatch errors, the building name-ambiguity machinery, the
desired*ByID family, and the *_id cell reads throughout; export writes the
parent id into the single cell and change-detection compares canonical names.

Interim: two buildings identical in (site, name) are no longer independently
addressable by the name-keyed grid/collision/capacity/move validators; #767
step 7 restores id-precision via the resolved-plan graph and re-adds the four
dropped id-disambiguation tests.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…767)

Canonicalizing a parent reference to a name loses id precision when two
buildings share a (site, name) pair — a case the DB permits via distinct
NULL site_ids. resolveReferences now records the resolved existing
building id in an internal companion cell next to the canonicalized name,
and the resolved rack/miner nodes and grid/collision/capacity/move
validators key on that id, falling back to (site, name) only for
same-import creates (whose uniqueness is enforced separately).

Restores the four id-disambiguation tests dropped when the single
reference-cell model landed, now expressed in that model.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Preview counts, the commit token, and (next) apply must agree on what
each row does. Update classification was derived three times — node
rename/move flags, a row-comparison count helper, and apply's rowsEqual —
which could disagree (e.g. a name-only row with a blank id column).

classifyTopologyActions now assigns every site/building/rack node its
create/update verb by comparing the canonicalized row against the live
comparable row across every editable column (the id column is identity,
not editable, so it is excluded). computeChanges reads those node actions;
the row-based count*Updates / countExistingRowUpdates* / rowKey helpers
are deleted.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Rewire the apply path to iterate the canonical resolved nodes instead of
re-deriving desired topology from raw CSV rows. applySites/applyBuildings/
applyRacks/applyMiners now switch on each node's classified action
(actionCreate/actionUpdate) and read the node's resolved fields, so
preview, commit-token, and apply share one derivation of "what changed".

Appliers prefer the resolved existing building id (node.buildingID) when
resolving placement parents, disambiguating two buildings that share a
(site, name) pair; they fall back to the canonical name lookup against the
running apply maps for same-import creates. Miner apply keys create/update
off node.renamed/node.moved rather than re-comparing row cells.

Single RunInTx atomicity and the existing store-orchestration/locking
bodies are preserved verbatim. Apply unit tests construct resolved nodes
directly; the move-before-delete integration test drives the real
resolveReferences + resolvePlan pipeline.

The old row-based helpers (existingRackByIDRow, rowsEqual,
desiredRackGridPosition, and the row twins of desiredRackZone) are now
orphaned and removed in the task-6 dead-code sweep.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
#767)

The remove-omitted delete set is computed against the caller-visible
snapshot (snap.miners), and RBAC-hidden rack members are tracked in a
separate hiddenRackMembers list — so a hidden miner is structurally
excluded from omission deletion. Add a regression test asserting that
invariant end-to-end through applyOmittedRows: a hidden miner in an
omitted rack is never passed to any unassign store call. The mock
controller fails on any call naming the hidden device, so the guarantee
is enforced by the absence of expectations for it.

This closes the #767 hidden-resource delete-guard task: investigation
confirmed the guard already holds (visible-only delete sets; validators
already fold hiddenRackMembers into slot/capacity/conflict checks; the
site delete path re-validates hidden curtailment/infrastructure
resources under lock). No behavior change was needed; this test makes
the delete-set invariant explicit and regression-proof.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ead row helpers (#767)

Commit token: hash the resolved plan's create/update node set (identity +
every mutable facet apply writes) alongside the omission counts, live
snapshot fingerprint, and mode — replacing the previous blob of raw CSV
rows plus the human-facing preview summary. Because apply executes the
same resolved plan, a token can no longer validate a topology that apply
would resolve differently. Token values change (acceptable per the plan
doc); the dry-run→commit staleness contract is unchanged. Add a
regression test proving the token moves when a CSV edit changes the
resolved plan even against an identical snapshot, complementing the
existing snapshot-drift test.

Dead-code sweep: remove the row-based apply helpers orphaned once apply
moved onto resolved nodes — existingRackByIDRow, rowsEqual,
parseInt32Field, desiredRackGridPosition, and the row twin of
desiredRackZone (the node twin desiredRackZoneForNode remains). Repoint
the zone-clearing test at the node twin.

Note: the dead OMISSION_MODE_LEAVE_IN_PLACE proto enum value still needs
deleting + regen, which is deferred (proto regen is unreliable in a
worktree).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@flesher
flesher requested a review from a team as a code owner July 23, 2026 20:45
Copilot AI review requested due to automatic review settings July 23, 2026 20:45
@github-actions github-actions Bot added documentation Improvements or additions to documentation javascript Pull requests that update javascript code client server labels Jul 23, 2026
@github-actions github-actions Bot added the review-policy: needs-review Managed by the Review Policy workflow. label Jul 23, 2026
@github-actions

github-actions Bot commented Jul 23, 2026

Copy link
Copy Markdown

🔐 Codex Security Review

Note: This is an automated security-focused code review generated by Codex.
It should be used as a supplementary check alongside human review.
False positives are possible - use your judgment.

Scope summary

  • Reviewed pull request diff only (fa57bd3fd3044e3800d1b3a91dff3422c4ef4898...1a919466ccc0041609b55d465cf9d17de0f14e97, exact PR three-dot diff)
  • Model: gpt-5.5

💡 Click "edited" above to see previous reviews for this PR.


Review Summary

Overall Risk: MEDIUM

Findings

[MEDIUM] Contradictory parent references are silently rewritten

  • Category: Reliability
  • Location: server/internal/domain/sitemap/service.go:2815
  • Description: During reference resolution, a RACK row first resolves the explicit site cell, but then a non-empty building cell wins and overwrites the site with the building's site without checking whether the explicit site disagreed. The same pattern exists for MINER rows when building or rack implies different ancestors. A CSV row can therefore say site A and building/rack under site B, and the importer will silently apply site B.
  • Impact: Operators can import a contradictory CSV and move racks or miners to a different site/building than the row explicitly stated, which is especially risky in destructive remove-omitted imports and automated CSV editing workflows.
  • Recommendation: Preserve the original explicit ancestor cells while resolving references. If a more-specific reference implies a site/building that conflicts with an explicitly supplied less-specific reference, return a validation error instead of overwriting the row.

[MEDIUM] LEAVE_IN_PLACE omission mode no longer produces a committable plan

  • Category: Other
  • Location: server/internal/domain/sitemap/service.go:1162
  • Description: The build-plan early return now triggers for every omission mode except REMOVE_OMITTED. Because ImportSiteMapCsv then returns omission_choice_required whenever omissions exist and mode is not REMOVE_OMITTED, an API caller that explicitly sends OMISSION_MODE_LEAVE_IN_PLACE can no longer receive changes or a commit token. The proto enum still exposes this mode.
  • Impact: Existing clients that relied on partial CSV imports while leaving omitted entities untouched are forced into a non-committable preview loop or must choose the destructive remove-omitted path. That is a behavioral regression in the public RPC contract.
  • Recommendation: Either restore the old behavior for OMISSION_MODE_LEAVE_IN_PLACE by only requiring a choice for UNSPECIFIED, or remove/deprecate the enum and return a clear validation error when callers send it.

Notes

I reviewed .git/codex-review.diff as the authoritative scope. I did not find changed-hunk evidence of auth, SQL injection, command injection, cryptostealing/pool hijack, or protobuf wire-format issues. I could not run go test ./server/internal/domain/sitemap because the sandbox is read-only and Go could not create a module cache.


Generated by Codex Security Review |
Triggered by: @flesher |
Review workflow run

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 77b9f05038

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread server/internal/domain/sitemap/service.go

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.

Pull request overview

Refactors the sitemap CSV import flow to resolve the uploaded CSV + live snapshot into a single canonical “resolved plan” representation that downstream stages (preview, validation, commit token, apply) can consume consistently. This also extracts the building rack-capacity invariant into the buildings domain so both the buildings service and sitemap import share the same logic.

Changes:

  • Introduces resolvedPlan / resolved node types plus plan-driven validation and change counting.
  • Extracts rack-capacity helpers (GridCapacity, RackCapacityExceeded) into server/internal/domain/buildings/models and updates callers.
  • Updates ProtoFleet’s import modal UX to remove the “leave omitted rows in place” option and treat omissions as a removal confirmation step; adds/updates characterization tests accordingly.

Reviewed changes

Copilot reviewed 7 out of 9 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
server/internal/domain/sitemap/service_test.go Updates sitemap import/export tests for the new single-cell reference model, plan-based tokening, and plan-driven apply helpers.
server/internal/domain/sitemap/resolved.go Adds the canonical resolved-plan types and a large portion of plan-driven validation and change classification logic.
server/internal/domain/sitemap/resolved_test.go Adds unit tests for resolved-plan identity classification, topology linking expectations, and omission-awareness behavior.
server/internal/domain/buildings/service.go Switches capacity math to shared helpers and uses RackCapacityExceeded for the assignment guard.
server/internal/domain/buildings/models/models.go Adds shared capacity helpers GridCapacity and RackCapacityExceeded.
server/internal/domain/buildings/models/models_test.go Adds unit tests for the new capacity helpers.
docs/plans/2026-07-22-767-sitemap-canonical-plan-tdd.md Adds a TDD documenting the canonical-plan refactor approach and constraints.
client/src/protoFleet/features/fleetManagement/components/SiteMapCsvImportModal.tsx Removes the “leave omitted rows” option and updates the omission prompt copy to a removal confirmation flow.

Comment thread server/internal/domain/sitemap/resolved.go Outdated
Comment thread server/internal/domain/sitemap/resolved.go
Comment thread server/internal/domain/sitemap/service_test.go
…s, sharpen errors, align guide (#767)

Resolves the automated review feedback on #808:

- Remove the never-read parent-pointer fields (resolvedBuilding.site,
  resolvedRack.building/site, resolvedMiner.rack/building/site) and the
  linkSite / indexSitesByIdentity / existingSitesByName machinery that
  only fed them. Nodes carry their parents as canonical name refs
  (siteRef/buildingRef) plus the resolved parent id; the file header now
  says so instead of claiming pointer links. (Copilot: misleading header.)

- Delete the unreachable site/building mismatch branches in
  validatePlacementConsistency. resolveReferences canonicalizes a miner's
  site/building to its rack's parents before the node is built (rack wins
  over building wins over site), so those branches could never fire; the
  precedence is now documented in place. (Codex: silent parent overwrite —
  precedence is intended, so the dead checks are removed rather than the
  behavior changed.)

- Include the offending device_identifier in the unknown-miner validation
  error. (Copilot.)

- Update the export guide (and its test) to describe remove-omitted as the
  only omission behavior; drop the stale leave-in-place sentence. (Copilot.)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@flesher

flesher commented Jul 23, 2026

Copy link
Copy Markdown
Contributor Author

🤖 Triaged both Codex Security Review findings:

Finding 1 — leave-in-place omission mode (MEDIUM): Working as intended. Remove-omitted is now the only committable omission mode (locked product decision, commit f3c2a7c). The commit path rejects any other mode, and the preview surfaces the omitted counts and requires explicit confirmation before removal. The dead OMISSION_MODE_LEAVE_IN_PLACE proto enum value is slated for deletion in a follow-up (proto regen is unreliable in this worktree, so the enum cleanup is tracked separately).

Finding 2 — silent parent-reference overwrite (MEDIUM): Fixed in 18421d4. This is intended precedence, not a silent bug: for a single row, a rack reference wins over a building reference, which wins over a site reference. The finding pointed at dead id-vs-name mismatch validation branches that could never fire under the single-reference-cell model. Those dead branches are now deleted and the rack-wins precedence is documented inline in validatePlacementConsistency.

Independent security review: LOW risk, no new vulnerabilities. Import stays inside a single RunInTx; the commit token is an optimistic-concurrency drift check recomputed and compared server-side. One pre-existing consideration noted — during import the plan reflects org-wide topology (sites/buildings/racks load org-wide, only miners are viewer-scoped), consistent with the accepted #520 decision; not changed here.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 18421d4f06

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread server/internal/domain/sitemap/service.go
flesher and others added 4 commits July 24, 2026 06:26
…#767)

Under REMOVE_OMITTED, validateRemoveOmittedReferences checked only that some
(site, name) BUILDING row remained. Two unassigned buildings can share a
(site, name) pair via distinct NULL site_ids, so a rack or miner referencing
the omitted twin by numeric id passed validation, was placed under that id by
applyRacks/applyMiners, and was then orphaned when applyOmittedRows deleted it
in the same transaction — regressing the id-qualified disambiguation path.

Check the resolved id preserved in refBuildingIDCell against the retained
BUILDING rows' own ids instead of the canonicalized name. Id references only
ever resolve to existing buildings (NAME: creates leave the cell nil), so
bare-id refs get the id check and name/create refs keep the name check.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
#767)

The canonical-plan refactor introduced switches on the new nodeAction and
refKind enums that relied on fall-through for the unhandled cases. The
exhaustive linter (default config, so a default case does not count) flagged
them. List the missing cases explicitly with no-op bodies — behavior is
unchanged:
- applySites: actionNone/actionDelete are no-ops (deletes run in applyOmittedRows).
- resolveSite/resolveBuilding/resolveRack: refUnassigned (blank cell) resolves to empty.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The "creates an automation" test waited for the modal to close, then
synchronously looked up the new row by text. The row list re-renders a tick
after the modal closes, so under CI load the lookup ran before the row
existed and failed with "Unable to find an element with the text: High LMP
spike" (empty-state DOM). Add the same `await screen.findByText(...)` guard
the sibling edit/delete test already uses for this documented race.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ToBuilding (#767)

capacity is already bound and guarded >0 by the enclosing if; reuse it
directly instead of calling RackCapacityExceeded, which recomputed
GridCapacity and re-checked >0. Behavior-identical; the shared helper
stays for the sitemap path.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
sitemodels "github.com/block/proto-fleet/server/internal/domain/sites/models"
)

// resolved.go builds the canonical resolved plan for a sitemap import: parsed

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.

nit: something like import_plan would have been clearer

Comment on lines +190 to +195
sites: desiredSiteSet(siteRows, target.sites),
buildingKeys: rowSetFromDesiredBuildings(buildingRows, target.buildings),
buildingsByKey: desiredBuildingMap(buildingRows, target.buildings),
buildingsByLayoutID: desiredBuildingLayoutIDMap(buildingRows, target.buildings),
buildingsByCapacityKey: desiredBuildingCapacityMap(buildingRows, target.buildings),
racksByLabel: desiredRackMap(rackRows, target.racks, buildingRows, target.buildings),

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.

I think we can simplify and DRY this up a bit, the helpers seem to be doing similar operations. Maybe call desiredBuildingList once, then iterate over the desiredBuildings and create the different maps (buildingsByKey, buildingsByLayoutID, buildingsByCapacityKey).
Can also pass buildingsByKey and buildingsByID to desiredRackMap so it can use them to resolve rack parents.

This way there will be fewer places to edit if we need to change the csv in the future

@github-actions github-actions Bot added review-policy: human-approved Managed by the Review Policy workflow. and removed review-policy: needs-review Managed by the Review Policy workflow. labels Jul 24, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

client documentation Improvements or additions to documentation javascript Pull requests that update javascript code review-policy: human-approved Managed by the Review Policy workflow. server

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants