mirror of
https://github.com/pbakaus/impeccable.git
synced 2026-09-15 07:36:50 +03:00
Fix: honor --target for nested products in non-monorepo repos (#377)
* Fix: honor --target for nested products in non-monorepo repos Closes #376. Resolve projectRoot from the target path when no monorepo marker is present, and inherit missing context files from the repo root when the active project is nested below it. Co-authored-by: Cursor <cursoragent@cursor.com> * Recognize nested-product context in .agents/context/ and docs/ fallback dirs Addresses PR #377 review: nearestTargetContextRoot only matched canonical PRODUCT.md/DESIGN.md directly in a directory, so nested products keeping context in the documented fallback locations were never selected. Reuse resolveLocalContextDir so the walk honors the same lookup order. Co-authored-by: Cursor <cursoragent@cursor.com> --------- Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -7,9 +7,13 @@
|
||||
* using the existing code as context.
|
||||
*
|
||||
* Path resolution (first match wins):
|
||||
* 1. Active project root, if PRODUCT.md or DESIGN.md is there
|
||||
* 1. Active project root, if PRODUCT.md or DESIGN.md is there. An explicit
|
||||
* --target selects the active project: the workspace child in a
|
||||
* monorepo, or the nearest directory around the target carrying
|
||||
* canonical context files in an ordinary repo (issue #376).
|
||||
* 2. Active project .agents/context/ then docs/
|
||||
* 3. Monorepo root context, using the same order, as a per-file fallback
|
||||
* 3. Repo root context, using the same order, as a per-file fallback
|
||||
* whenever the active project is nested below it
|
||||
* 4. $IMPECCABLE_CONTEXT_DIR (absolute or cwd-relative) — power-user
|
||||
* escape hatch, only consulted when defaults are empty
|
||||
* 5. Active project root as a "nothing found" default
|
||||
@@ -88,7 +92,10 @@ function resolveContext(cwd = process.cwd(), options = {}) {
|
||||
const absCwd = path.resolve(cwd);
|
||||
const project = resolveProject(absCwd, options);
|
||||
const projectContextDir = resolveLocalContextDir(project.projectRoot);
|
||||
const rootContextDir = project.isMonorepo && project.repoRoot !== project.projectRoot
|
||||
// Per-file inheritance from the repo root whenever the active project is
|
||||
// nested below it: monorepo workspace children and explicit-target nested
|
||||
// products in ordinary repos behave the same way.
|
||||
const rootContextDir = project.repoRoot !== project.projectRoot
|
||||
? resolveLocalContextDir(project.repoRoot)
|
||||
: null;
|
||||
|
||||
@@ -165,7 +172,7 @@ function resolveProject(cwd = process.cwd(), options = {}) {
|
||||
if (!repoRoot) {
|
||||
return {
|
||||
targetDir,
|
||||
projectRoot: absCwd,
|
||||
projectRoot: nearestTargetContextRoot(absCwd, targetDir) || absCwd,
|
||||
repoRoot: absCwd,
|
||||
isMonorepo: false,
|
||||
};
|
||||
@@ -471,6 +478,31 @@ function isExcludedByWorkspacePattern(relSegments, patterns) {
|
||||
});
|
||||
}
|
||||
|
||||
// An explicit --target in an ordinary (non-monorepo) repository must still
|
||||
// select a nested product's own context (issue #376). Walk from the target up
|
||||
// to — but not including — the invocation root and return the nearest
|
||||
// directory carrying context files, in the canonical spot or a fallback dir
|
||||
// (resolveLocalContextDir covers both). Context files only, not package.json:
|
||||
// without the monorepo root-context fallback, a package.json marker would
|
||||
// strand targets inside plain subpackages away from the root PRODUCT.md. The
|
||||
// cwd's own fallback context dirs (.agents/context, docs) hold the root
|
||||
// project's context, not a nested product, so they never count.
|
||||
// Returns null when nothing nested is found, keeping the cwd default.
|
||||
function nearestTargetContextRoot(absCwd, targetDir) {
|
||||
if (!isPathInside(targetDir, absCwd)) return null;
|
||||
const rootFallbackDirs = FALLBACK_DIRS.map((rel) => path.resolve(absCwd, rel));
|
||||
let dir = path.resolve(targetDir);
|
||||
while (dir && dir !== absCwd) {
|
||||
if (!rootFallbackDirs.includes(dir) && resolveLocalContextDir(dir)) {
|
||||
return dir;
|
||||
}
|
||||
const parent = path.dirname(dir);
|
||||
if (parent === dir) break;
|
||||
dir = parent;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function nearestProjectLikeRoot(repoRoot, targetDir) {
|
||||
let dir = path.resolve(targetDir);
|
||||
const stop = path.resolve(repoRoot);
|
||||
|
||||
+73
-2
@@ -278,11 +278,12 @@ describe('loadContext (monorepo project context)', () => {
|
||||
|
||||
it('does not reuse stale project resolution after workspace markers change', () => {
|
||||
write('PRODUCT.md', '# Root product\n');
|
||||
write('apps/dashboard/PRODUCT.md', '# Dashboard product\n');
|
||||
write('apps/dashboard/package.json', JSON.stringify({ name: 'dashboard' }, null, 2));
|
||||
write('apps/dashboard/src/App.jsx', 'export default null;\n');
|
||||
|
||||
const before = loadContext(scratch, { targetPath: 'apps/dashboard/src/App.jsx' });
|
||||
assert.equal(before.projectRoot, scratch);
|
||||
assert.equal(before.isMonorepo, false);
|
||||
assert.match(before.product, /Root product/);
|
||||
|
||||
write('package.json', JSON.stringify({
|
||||
@@ -292,7 +293,77 @@ describe('loadContext (monorepo project context)', () => {
|
||||
|
||||
const after = loadContext(scratch, { targetPath: 'apps/dashboard/src/App.jsx' });
|
||||
assert.equal(after.projectRoot, path.join(scratch, 'apps', 'dashboard'));
|
||||
assert.match(after.product, /Dashboard product/);
|
||||
assert.equal(after.isMonorepo, true);
|
||||
assert.match(after.product, /Root product/);
|
||||
});
|
||||
|
||||
it('resolves an explicit target onto a nested product context in a non-monorepo repo', () => {
|
||||
write('nested/product/PRODUCT.md', '# Nested product\n');
|
||||
write('nested/product/DESIGN.md', '# Nested design\n');
|
||||
write('nested/product/file.ts', 'export const x = 1;\n');
|
||||
|
||||
const ctx = loadContext(scratch, { targetPath: 'nested/product/file.ts' });
|
||||
assert.equal(ctx.isMonorepo, false);
|
||||
assert.equal(ctx.projectRoot, path.join(scratch, 'nested', 'product'));
|
||||
assert.equal(ctx.repoRoot, scratch);
|
||||
assert.match(ctx.product, /Nested product/);
|
||||
assert.match(ctx.design, /Nested design/);
|
||||
assert.equal(ctx.productPath, path.join('nested', 'product', 'PRODUCT.md'));
|
||||
assert.equal(ctx.designPath, path.join('nested', 'product', 'DESIGN.md'));
|
||||
});
|
||||
|
||||
it('resolves a nested target whose context lives in .agents/context/', () => {
|
||||
write('nested/product/.agents/context/PRODUCT.md', '# Nested product\n');
|
||||
write('nested/product/file.ts', 'export const x = 1;\n');
|
||||
|
||||
const ctx = loadContext(scratch, { targetPath: 'nested/product/file.ts' });
|
||||
assert.equal(ctx.isMonorepo, false);
|
||||
assert.equal(ctx.projectRoot, path.join(scratch, 'nested', 'product'));
|
||||
assert.match(ctx.product, /Nested product/);
|
||||
assert.equal(ctx.productPath, path.join('nested', 'product', '.agents', 'context', 'PRODUCT.md'));
|
||||
});
|
||||
|
||||
it('resolves a nested target whose context lives in docs/', () => {
|
||||
write('nested/product/docs/DESIGN.md', '# Nested design\n');
|
||||
write('nested/product/file.ts', 'export const x = 1;\n');
|
||||
|
||||
const ctx = loadContext(scratch, { targetPath: 'nested/product/file.ts' });
|
||||
assert.equal(ctx.isMonorepo, false);
|
||||
assert.equal(ctx.projectRoot, path.join(scratch, 'nested', 'product'));
|
||||
assert.match(ctx.design, /Nested design/);
|
||||
assert.equal(ctx.designPath, path.join('nested', 'product', 'docs', 'DESIGN.md'));
|
||||
});
|
||||
|
||||
it('does not treat the root fallback context dirs as a nested product', () => {
|
||||
write('.agents/context/PRODUCT.md', '# Root product\n');
|
||||
write('.agents/context/notes/file.md', '# Notes\n');
|
||||
|
||||
const ctx = loadContext(scratch, { targetPath: '.agents/context/notes/file.md' });
|
||||
assert.equal(ctx.projectRoot, scratch);
|
||||
assert.match(ctx.product, /Root product/);
|
||||
});
|
||||
|
||||
it('inherits missing root context per-file for a nested target in a non-monorepo repo', () => {
|
||||
write('DESIGN.md', '# Root design\n');
|
||||
write('nested/product/PRODUCT.md', '# Nested product\n');
|
||||
write('nested/product/file.ts', 'export const x = 1;\n');
|
||||
|
||||
const ctx = loadContext(scratch, { targetPath: 'nested/product/file.ts' });
|
||||
assert.equal(ctx.projectRoot, path.join(scratch, 'nested', 'product'));
|
||||
assert.match(ctx.product, /Nested product/);
|
||||
assert.match(ctx.design, /Root design/);
|
||||
assert.equal(ctx.designPath, 'DESIGN.md');
|
||||
});
|
||||
|
||||
it('keeps the repo root project for targets without nearby context files', () => {
|
||||
write('PRODUCT.md', '# Root product\n');
|
||||
write('src/App.jsx', 'export default null;\n');
|
||||
|
||||
const ctx = loadContext(scratch, { targetPath: 'src/App.jsx' });
|
||||
assert.equal(ctx.isMonorepo, false);
|
||||
assert.equal(ctx.projectRoot, scratch);
|
||||
assert.match(ctx.product, /Root product/);
|
||||
assert.equal(ctx.productPath, 'PRODUCT.md');
|
||||
});
|
||||
|
||||
it('does not escape a nested git repo to an ancestor workspace', () => {
|
||||
|
||||
Reference in New Issue
Block a user