Reject archived parent targets in Board JSON hierarchy imports - #2994
Conversation
Hand-crafted version-3 Board JSON could name a card with IsArchived=true as another card's ParentCardId. Import set the relationship and CardHierarchy.Validate accepted it, because that validator only checks existence, board scope, cycles and depth. The create, update and proposal lanes all refuse an archived parent, and ordinary archival detaches every direct child, so no clean export can produce that graph; restoring the imported parent then unexpectedly retained those links. ImportBoardAsync now rejects such a payload in the existing whole-payload validation pass, before the board, columns or cards are constructed, so a rejected graph can never leave a partial board. It reuses the ValidationError code the import already uses for missing-parent, cycle and depth failures (HTTP 400) and the wording of CardService.ValidateActiveParent. Archived children whose parent stays active still import, and plain/version-2 payloads are unaffected. Tests: three Application-level cases (rejected archived parent with nothing created and the transaction rolled back, archived child under an active parent accepted, version-2 envelope still imported) and an archivedParent case on the existing Api atomicity theory. Both rejection tests were confirmed red without the fix.
Codex Review SummaryThis comment shows the latest Codex review activity on this pull request.
ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings. |
|
Fresh-context independent review at exact head Verified by the reviewer from source (read-only):
Author-run proof at this head: full Non-blocking notes, recorded here rather than as commits (law 2c):
Merge gate: hosted checks at this head plus the 3-minute aging floor; will merge with a merge commit once green. |
What
BoardJsonExportImportService.ImportBoardAsyncnow rejects a version-3 Board JSON payload that names an archived card as another card's parent. The check sits in the existing whole-payload validation pass, immediately after the source-ID loop and before the board, columns or cards are constructed.Why
Follow-up from PR #2965 review comment 3984069198 (source-confirmed MEDIUM contract gap).
Hand-crafted version-3 Board JSON could set
IsArchived=trueon a card and still name it as another card'sParentCardId. Import calledSetParentbeforeArchive, andCardHierarchy.Validateaccepted the graph because that validator only checks existence, board scope, cycles and depth. The create (CardService.ValidateActiveParent), update, and proposal (ProposalHierarchyValidator) lanes all refuse an archived parent, and ordinary archival detaches every direct child, so no clean export can produce this graph. Restoring the imported parent then unexpectedly retained those links.How
ErrorCodes.ValidationError, the same code the import already throws for missing-parent, duplicate-source-ID, cycle and depth failures, so the HTTP surface stays 400 and the existingcatch (DomainException)->RollbackTransactionAsyncpath is unchanged.CardService.ValidateActiveParentandProposalHierarchyValidator, prefixed with the offending card title the way the import's other messages are.new Board(...)means a rejected payload never reaches board/column/card creation at all; the transaction rollback is a second line of defence rather than the only one.IsArchivedflags is equivalent to validating the completed graph, becausecard.Archive()in the import loop is driven by exactly that flag and nothing else.Scope
CardHierarchy,CardServiceand the proposal lanes are untouched.Checks run
dotnet test backend/tests/Taskdeck.Application.Tests/Taskdeck.Application.Tests.csproj -c Release -m:1 --filter "FullyQualifiedName~ExportImport"— 78/78 passed.dotnet test backend/tests/Taskdeck.Api.Tests/Taskdeck.Api.Tests.csproj -c Release -m:1 --filter "FullyQualifiedName~CardHierarchyContractTests"— 11/11 passed.dotnet test backend/Taskdeck.sln -c Release -m:1(thebackend/AGENTS.mdrequired check) — green, exit 0: Domain 1674/1674, Application 4328/4328, Api 3143 passed / 4 skipped, Cli 243/243, Architecture 28 passed / 1 skipped, Integration 7 passed / 29 skipped. 9423 passed, 34 skipped, 0 failed.node scripts/check-docs-governance.mjs— passed.node scripts/check-doc-links.mjs— passed (693 files, 0 broken links).ImportBoardAsync_RejectsArchivedParentTarget_BeforeCreatingAnything— failed with the source change reverted.ImportRemapsChildBeforeParentAndRejectsBadGraphsAtomically(shape: "archivedParent")— failed with the source change reverted.Tests added
ImportBoardAsync_RejectsArchivedParentTarget_BeforeCreatingAnythingValidationErrorand the shared wording;Boards.AddAsync,Columns.AddAsync,Cards.AddAsync,SaveChangesAsyncandCommitTransactionAsyncall never called;RollbackTransactionAsynccalled once. No partial board.ImportBoardAsync_ImportsArchivedChildWhenItsParentStaysActiveImportBoardFromJsonAsync_StillImportsVersion2PayloadWithoutHierarchytaskdeck-boardversion-2 envelope still imports its column and card and commits.CardHierarchyContractTests...(shape: "archivedParent")POST /api/import/boards: 400 and the persisted board count is unchanged.The Api case is one
[InlineData]plus one named argument on the existing atomicity theory, which already asserted "400 and no board persisted" for the missing/duplicate/cycle shapes.Docs
docs/product/CARD_HIERARCHY.md— one sentence added to the existing Board JSON import paragraph stating the new rule and the preserved archived-child case.docs/STATUS.mddeliberately untouched: it has no current hierarchy block to append to and PR #2947 owns STATUS reconciliation right now.NOT verified
CardService.ValidateActiveParentandProposalHierarchyValidator; there is no shared constant tying them together, so the three strings can still drift independently. Not fixed here to keep the change localized.ValidationError; no test pins which message wins.BoardJsonExportImportService.cs(PR Add multiple card assignments and explicit import mapping #2977, card assignments). This branch only inserts a block and rewrites no existing line, but the merge order has not been exercised.Closes #2966