Follow symlinked child repos in workspace discovery - #328
Conversation
discoverWorkspaceChildren() only accepted entries for which Dirent.isDirectory() is true, which is false for a symlink, so a child that is a symlink to a git repo was silently left out of a workspace build. Ticket workspaces assembled from worktrees plus symlinks are a common way to give one session exactly the repos a change touches. Treat a symlink as a child when it resolves to a directory, following it one level like walkDir follows a symlinked root. Dangling links, links to files, and links that point back at the workspace root or one of its ancestors are not children. Fixes trailhq#319
🌱 graft blast radius1 area changed → 2 areas can be affected. 10 dependent symbols, depth 2. flowchart TB
A0(("Workspace Federation<br/>9 symbols"))
A1(("CLI Entry Point<br/>1 symbol"))
classDef reached fill:#D9EDF3,stroke:#3AA7C9,stroke-width:1.5px,color:#0E313C;
class A0,A1 reached;
Who knows this code — 2 people across 3 areas
Ownership is git history over each area's own files, weighted towards recent work (120-day half-life). Merge commits and bots are dropped, and you are dropped from your own PR. A name with no All 10 dependent symbols, grouped by areaWorkspace Federation — 9 symbols in 2 files
CLI Entry Point — 1 symbol in 1 file
Test signal per changed area — 1 ✓Reached = a node under a test path has a resolved edge into the changed symbol. It undercounts anything called indirectly — through a CLI, a spawned process or a dynamic import — so read a low ratio as “look here”, never as a coverage gate.
2 test suites also reference this code3 symbols, kept out of the diagram and the table so they cannot crowd out the areas a reviewer has to look at.
Open the interactive graph → — click an area to see its dependent symbols at file:line. |
- generic-extract.test.ts: the union merge of trailhq#196, trailhq#200 and trailhq#201 spliced their appended tests into one another and left the file unterminated. Rebuilt as trailhq#196's file plus trailhq#200's and trailhq#201's appended blocks verbatim. - The trailhq#139 test asserted that a cached parse failure is replayed without a re-parse; trailhq#317 (fixes trailhq#312) deliberately re-parses it instead and did not update the test. It now asserts the re-parse and that the error still comes back. - The trailhq#319 symlink test (trailhq#328) needed symlink privilege on Windows. Directory links are junctions there, as test/graph-root and test/ingest-fs already do, and the file-link case is skipped with a diagnostic when it cannot be created. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Fixes #319
discoverWorkspaceChildren()filteredreaddirSync(root, { withFileTypes: true })withe.isDirectory(), which isfalsefor a symlink, so a child that is a symlink to a git repo was silently left out ofgraft build <workspace>(not built, not inworkspace.json, no warning). A git worktree (.gitfile) was fine, andgraft build ws/repoC-linkon its own worked, which made the omission surprising.Change: an entry is a workspace child when it is a real directory or a symlink that resolves to a directory. A symlinked child is followed one level, consistent with how
walkDircanonicalises a symlinked root once. Dangling links, links to files, and links that resolve to the workspace root or one of its ancestors (a cycle) are not children.Test:
discoverWorkspaceChildren follows a symlinked child repo, not dangling or cyclic links (#319)intest/graph-scopes.test.tsbuilds a workspace with a real repo, a symlink to a repo outside the workspace, a dangling link, a link to a file and a link back to the workspace itself, and expects["repoA", "repoC-link"]. It fails onmain(["repoA"]) and passes with the change; the rest ofgraph-scopes.test.ts(16 tests) passes.