Surface the build-path finding in doctor, and keep cwd out of the lookup

Round two of review findings, all four valid.

`doctor` builds its own finding list and never called `checkBuildPathUnset`,
so `config-build-path-unset` could not appear in the report even though
doctor.md documents it. That is also the only path left once stalenessCheck is
off, which is exactly when someone is looking for it.

The lookup chain included `process.cwd()`, which lets an ambient invoking
directory decide another project's workflow: run from workspace A with
--target resolving onto workspace B, and B inherited A's buildPath ahead of
the repository default. The chain is now the resolved project then the repo
root, matching `checkBuildPathUnset` exactly; cwd stands in only when no
project resolved at all.

Two prose contradictions, both mine. new-work said to write the value "when
the user says yes" and then to "record the answer either way", which reads as
persist-on-yes-only and leaves the decline to be asked again next session. It
now says the write always happens and the answer picks the value. The README
still pointed existing projects at re-running init, which is the problem this
PR exists to solve; it now names the toggle as the migration path.

The workspace-isolation test earned a correction of its own: the first version
passed a relative --target, which resolves against the caller's cwd and puts
projectRoot back on the calling workspace, so it asserted nothing.

Written with AI assistance (Claude Code).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Paul Bakaus
2026-08-13 22:41:32 -04:00
co-authored by Claude Opus 5
parent c0e7f2d778
commit 816ffe92d0
5 changed files with 36 additions and 7 deletions
+10 -5
View File
@@ -1299,14 +1299,19 @@ function readBuildPathAt(root) {
return value ? { value, source } : null;
}
// Roots in precedence order, nearest first: the active workspace decides, and
// Roots in precedence order, nearest first: the resolved project decides, and
// the repo root is the fallback a monorepo commits once for every app in it.
// `checkBuildPathUnset` already reads both, so leaving repoRoot out here made
// the two disagree: the finding stayed silent because a value existed while the
// directive never named it, which is the one combination nobody can debug.
// `checkBuildPathUnset` reads exactly these two, and the pair has to match:
// when they disagree the finding goes silent because a value exists while the
// directive never names it, which is the one combination nobody can debug.
//
// The invoking directory is deliberately not in the chain. With `--target`
// selecting another workspace, cwd is the caller's app, not the target's, and
// letting it rank above the repo root hands one workspace another's workflow.
// It stands in only when no project resolved at all.
function appendBuildPathDirective(parts, ctx) {
const roots = [...new Set(
[ctx?.projectRoot, process.cwd(), ctx?.repoRoot].filter(Boolean).map((root) => path.resolve(root)),
[ctx?.projectRoot || process.cwd(), ctx?.repoRoot].filter(Boolean).map((root) => path.resolve(root)),
)];
for (const root of roots) {
const found = readBuildPathAt(root);
+2
View File
@@ -33,6 +33,7 @@ import {
stampProductSchema,
} from './lib/artifact-schema.mjs';
import {
checkBuildPathUnset,
checkConfig,
checkDesignSidecar,
checkNativePlatformEvidence,
@@ -120,6 +121,7 @@ async function collect(cwd, targetOptions) {
...checkDesignDrift({ designPath: absDesignPath, projectRoot }),
...checkDesignCoverage({ design: ctx.design, designPath: ctx.designPath, parseDesignMd }),
...checkConfig({ projectRoot, repoRoot: ctx.repoRoot }),
...checkBuildPathUnset({ projectRoot, repoRoot: ctx.repoRoot, product: ctx.product }),
...checkDetectorIgnores({ projectRoot, knownRuleIds }),
...checkSurfaceBriefs({ candidates: ctx.surfaceBriefCandidates, projectRoot }),
...checkHookInstallation({