fix(#1414): resolve project root from descendant via nearest ancestor .planning/ (Resolution Provenance P1) (#1423)
Add heuristic (4) to findProjectRoot: after heuristics (1)-(3) (sub_repos, multiRepo, .git+parent-.planning/) are exhausted without a match, perform a second bounded walk-up within FIND_PROJECT_ROOT_MAX_DEPTH to locate the nearest ancestor directory containing a .planning/ subdirectory. Returns that ancestor as the project root, so loadConfig finds the correct config instead of falling through to defaults when gsd-tools is invoked from a plain descendant subdirectory of a single-repo project (#1366 cwd-drift gap). Ordering is load-bearing: the new walk runs AFTER the existing loop so sub_repos workspaces (where a child sub-repo may have its own .planning/) still resolve correctly to the parent workspace. The existing own-.planning/ guard (heuristic 0, #1362) and the depth bound (FIND_PROJECT_ROOT_MAX_DEPTH=10) are both preserved unchanged. Part of #1411 Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -1,10 +1,11 @@
|
||||
/**
|
||||
* Project-Root Resolution Module — resolves a project root from a starting
|
||||
* directory by walking the ancestor chain and applying four heuristics:
|
||||
* directory by walking the ancestor chain and applying five heuristics:
|
||||
* (0) own .planning/ guard (#1362)
|
||||
* (1) parent .planning/config.json sub_repos
|
||||
* (2) legacy multiRepo: true + ancestor .git
|
||||
* (3) .git heuristic with parent .planning/
|
||||
* (4) nearest ancestor .planning/ (#1414, Resolution Provenance P1)
|
||||
* Bounded by FIND_PROJECT_ROOT_MAX_DEPTH ancestors. Sync I/O.
|
||||
*
|
||||
* ADR-457 build-at-publish: the hand-written bin/lib/project-root.cjs
|
||||
@@ -111,5 +112,30 @@ export function findProjectRoot(startDir: string): string {
|
||||
depth += 1;
|
||||
}
|
||||
|
||||
// Heuristic (4): nearest ancestor .planning/ — last resort before fallback.
|
||||
// Runs only after heuristics (1)–(3) have been exhausted without a match,
|
||||
// ensuring sub_repos / multiRepo / .git-based resolution always wins when
|
||||
// applicable. Walks upward again within the same FIND_PROJECT_ROOT_MAX_DEPTH
|
||||
// bound; returns the nearest ancestor directory that contains a .planning/
|
||||
// subdirectory so config resolves correctly when invoked from a plain
|
||||
// descendant of a single-repo project. (#1414)
|
||||
let dir2 = resolvedStart;
|
||||
let depth2 = 0;
|
||||
while (dir2 !== fsRoot && depth2 < FIND_PROJECT_ROOT_MAX_DEPTH) {
|
||||
const parent2 = path.dirname(dir2);
|
||||
if (parent2 === dir2) break;
|
||||
try {
|
||||
const candidatePlanning = parent2 + path.sep + '.planning';
|
||||
if (fs.existsSync(candidatePlanning) && fs.statSync(candidatePlanning).isDirectory()) {
|
||||
return parent2;
|
||||
}
|
||||
} catch {
|
||||
// ignore fs errors and continue walking
|
||||
}
|
||||
if (parent2 === home) break;
|
||||
dir2 = parent2;
|
||||
depth2 += 1;
|
||||
}
|
||||
|
||||
return startDir;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user