Skip to content

fix(doctor): fix orphan cross-attribution for nested scoped extensions (swamp-club#628)#1570

Merged
stack72 merged 1 commit into
mainfrom
worktree-628
Jun 11, 2026
Merged

fix(doctor): fix orphan cross-attribution for nested scoped extensions (swamp-club#628)#1570
stack72 merged 1 commit into
mainfrom
worktree-628

Conversation

@stack72

@stack72 stack72 commented Jun 11, 2026

Copy link
Copy Markdown
Contributor

Summary

  • Fix extractTopLevelRoot() to handle nested scoped extension names with 3+
    segments (e.g. @swamp/aws/iam, @swamp/aws/s3)
  • The function hardcoded a 2-segment assumption for scoped names, causing
    sibling nested extensions to extract to the same root directory and
    cross-attribute each other's files as false orphans
  • Add extensionName parameter so the function uses the actual segment count
    to determine the correct per-extension root depth

Closes #628

Test Plan

  • Unit tests for extractTopLevelRoot with 3-segment scoped names
  • Integration test verifying sibling nested extensions don't cross-attribute
    orphans
  • All existing tests updated and passing
  • Reproduced the bug in a scratch repo (/tmp/swamp-repro-issue-628) and
    verified the compiled binary reports 0 false orphans after the fix
  • deno check, deno lint, deno fmt, deno run test all pass

🤖 Generated with Claude Code

#628)

extractTopLevelRoot() hardcoded a 2-segment assumption for scoped extension
names (@scope/name), causing nested names like @swamp/aws/iam and @swamp/aws/s3
to extract to the same root directory. This made detectOrphanFiles() walk both
extensions' files under a shared root and cross-attribute each sibling's files
as orphans of the other.

Add an extensionName parameter so the function uses the actual segment count
from the extension name to determine the correct per-extension root depth.

Co-authored-by: crudec <crudec@users.noreply.github.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

@github-actions github-actions 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.

CLI UX Review

Blocking

None.

Suggestions

None.

Verdict

PASS — This PR touches only internal library functions (extractTopLevelRoot in layout.ts and its call site in doctor.ts). No commands, renderers, output formatters, help text, error messages, or JSON output shapes were changed. The fix silently corrects false-positive orphan reports for users with nested scoped extensions (e.g. @swamp/aws/iam), which is a transparent UX improvement — users simply stop seeing spurious warnings they should never have seen.

@github-actions github-actions 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.

Code Review

No blocking issues found. This is a well-scoped, correct bug fix.

Summary

The fix replaces a hardcoded 2-segment assumption in extractTopLevelRoot (which assumed scoped names are always @scope/name) with using the actual extensionName segment count. This correctly handles 3+ segment nested scoped extensions like @swamp/aws/iam and @swamp/aws/s3, which previously resolved to the same root and cross-attributed each other's files as false orphans.

What I verified:

  • extractTopLevelRoot is only called from doctor.ts:254 (plus tests) — the single call site is correctly updated to pass extensionName from the lockfile iteration
  • The function is internal to libswamp (not exported via mod.ts), so the signature change has no public API impact
  • New unit tests cover the 3-segment case directly, and a new integration-style test verifies sibling nested extensions don't cross-attribute orphans
  • All existing tests are updated with the new parameter
  • License headers present on all modified files
  • The .catch(() => {}) cleanup pattern is correctly used in the new temp-dir test
  • DDD: the change stays within the extensions bounded context; extractTopLevelRoot remains a pure stateless function — good domain design
  • No any types, no fire-and-forget promises, no libswamp import boundary violations

Suggestions

  1. The extensionName parameter is trusted to match the file path structure (i.e., the extension name segments should be a prefix of the path under pulled-extensions/). This is fine since the only caller iterates the lockfile map where keys and file paths are consistent by construction, but the implicit coupling is worth being aware of if more callers are added later.

@github-actions github-actions 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.

Adversarial Review

Critical / High

None.

Medium

None.

Low

  1. layout.ts:215 — Dropped empty-segment guard for bare-prefix file paths.
    The old code had if (segments.length === 0 || segments[0].length === 0) return null; which returned null when filePath was exactly .swamp/pulled-extensions/ (empty rest). The new code's guard segments.length < nameSegments.length does not catch this: "".split("/") yields [""] (length 1), and any valid extensionName also has ≥1 segments, so 1 < 1 is false, and the function returns .swamp/pulled-extensions/ instead of null. Not reachable in practice — lockfile files[] entries are always full file paths, never bare directory prefixes — but the semantic change exists. No action needed.

Verdict

PASS. The fix is correct and well-targeted. extensionName is always sourced from the same lockfile entry as the file paths (detectOrphanFiles iterates Object.entries(upstreamMap) and passes both from the same entry), so the trust placed on name/path consistency is justified. The new segment-count approach correctly generalises from the old hardcoded 2-segment assumption to N-segment scoped names. Tests cover the new 3-segment case, the existing 2-segment case, and the unscoped 1-segment case. Only caller updated, no public API surface affected (extractTopLevelRoot is not exported from mod.ts).

@stack72 stack72 merged commit f7ad0af into main Jun 11, 2026
11 checks passed
@stack72 stack72 deleted the worktree-628 branch June 11, 2026 19:12
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

swamp repo upgrade replaces entire CLAUDE.md with generic template instead of merging

1 participant