Skip to content

feat(core): add TaskFileResolver primitive; refactor show target --check to use it#35583

Open
polygraph-snapshot-app[bot] wants to merge 15 commits into
masterfrom
feat/check-sandbox-report
Open

feat(core): add TaskFileResolver primitive; refactor show target --check to use it#35583
polygraph-snapshot-app[bot] wants to merge 15 commits into
masterfrom
feat/check-sandbox-report

Conversation

@polygraph-snapshot-app

@polygraph-snapshot-app polygraph-snapshot-app Bot commented May 5, 2026

Copy link
Copy Markdown
Contributor

Summary

Adds a small generic TaskFileResolver primitive (exported from devkit-internals,
not devkit-exports) that lets callers ask whether a workspace-relative path is a
declared input or output of a given task. Refactors nx show target ... --check
(both inputs and outputs) to use it instead of duplicating the input/output
reconciliation logic.

This supersedes the earlier verifySandboxViolations API: that high-level helper
has been removed in favor of this lower-level primitive. Consumers (the nx-cloud
light client) own their own report schemas and call the primitive per file.

API

interface TaskFileResolver {
  isInput(taskId: string, file: string): boolean;
  isOutput(taskId: string, file: string): boolean;
  getInputs(taskId: string): string[];
  getRawInputs(taskId: string): HashInputs;
  getOutputs(taskId: string): string[];
  /**
   * Static validation for `dependentTasksOutputFiles` inputs: matches a path
   * against each declared dep-outputs glob ANDed with each upstream task's
   * declared outputs. Works without the upstream having actually run.
   */
  matchesDependentTaskOutputs(taskId: string, file: string): boolean;
}
function createTaskFileResolver(opts: {
  projectGraph: ProjectGraph;
  nxJson?: NxJsonConfiguration;
  workspaceRoot?: string;
}): Promise<TaskFileResolver>;

Exported from packages/nx/src/devkit-internals.ts.

isInput() accepts a path as an input if any of the following hold:

  1. Path is in HashInputs.files (resolved self-inputs).
  2. Path is in HashInputs.depOutputs (materialized — only populated after
    upstream tasks have run).
  3. Path matches a dependentTasksOutputFiles glob declared on the task AND
    lies inside the declared outputs of an upstream task in the task graph
    (honors transitive: true|false). This is the static check that lets
    sandbox-report verification work without first running the dependency.

Files

File Change
packages/nx/src/hasher/task-file-resolver.ts added — primitive impl
packages/nx/src/hasher/task-file-resolver.spec.ts added — unit tests
packages/nx/src/hasher/verify-sandbox-violations.ts deleted
packages/nx/src/hasher/verify-sandbox-violations.spec.ts deleted
packages/nx/src/devkit-internals.ts exposes createTaskFileResolver
packages/nx/src/devkit-exports.ts removes verifySandboxViolations exports
packages/nx/src/command-line/show/show-target/inputs.ts refactored — uses resolver
packages/nx/src/command-line/show/show-target/outputs.ts refactored — uses resolver

Test plan

  • Unit tests for the resolver primitive (23 tests passing locally — including
    6 new tests covering dependentTasksOutputFiles static validation).
  • Unit tests for show target inputs|outputs (25 tests passing locally).
  • CI build / lint / full jest suite.

Linked PR

Consumed by nrwl/ocean#11134, which owns the SandboxReport schema and
iterates the report calling resolver.isInput / resolver.isOutput per file.

@netlify

netlify Bot commented May 5, 2026

Copy link
Copy Markdown

Deploy Preview for nx-docs ready!

Name Link
🔨 Latest commit 9a5e3cb
🔍 Latest deploy log https://app.netlify.com/projects/nx-docs/deploys/6a4d5d30ad64ab0008babb55
😎 Deploy Preview https://deploy-preview-35583--nx-docs.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.

To edit notification comments on pull requests, go to your Netlify project configuration.

@netlify

netlify Bot commented May 5, 2026

Copy link
Copy Markdown

Deploy Preview for nx-dev ready!

Name Link
🔨 Latest commit 9a5e3cb
🔍 Latest deploy log https://app.netlify.com/projects/nx-dev/deploys/6a4d5d3160e2b200082436c0
😎 Deploy Preview https://deploy-preview-35583--nx-dev.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.

To edit notification comments on pull requests, go to your Netlify project configuration.

@nx-cloud

nx-cloud Bot commented May 5, 2026

Copy link
Copy Markdown
Contributor

View your CI Pipeline Execution ↗ for commit 98a1780

Command Status Duration Result
nx affected --targets=lint,test,build,e2e,e2e-c... ✅ Succeeded 9m 50s View ↗
nx run-many -t check-imports check-lock-files c... ✅ Succeeded 4s View ↗
nx-cloud record -- pnpm nx-cloud conformance:check ✅ Succeeded 58s View ↗
nx build workspace-plugin ✅ Succeeded <1s View ↗
nx-cloud record -- nx sync:check ✅ Succeeded 18s View ↗
nx-cloud record -- nx format:check ✅ Succeeded 6s View ↗

☁️ Nx Cloud last updated this comment at 2026-07-07 20:24:34 UTC

nx-cloud[bot]

This comment was marked as outdated.

@AgentEnder AgentEnder force-pushed the feat/check-sandbox-report branch from 9ec92af to 8378d0e Compare May 6, 2026 20:51
@AgentEnder AgentEnder changed the title feat(core): expose verifySandboxViolations API for sandbox-report checking feat(core): add TaskFileResolver primitive; refactor show target --check to use it May 6, 2026
nx-cloud[bot]

This comment was marked as outdated.

nx-cloud[bot]

This comment was marked as outdated.

@AgentEnder AgentEnder self-assigned this May 11, 2026
Comment on lines +23 to +25
const taskId = t.configuration
? `${t.projectName}:${t.targetName}:${t.configuration}`
: `${t.projectName}:${t.targetName}`;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We have utils to create the task id already, don't do it ad-hoc like this

Comment on lines +54 to +66
function parseTaskId(taskId: string): {
project: string;
target: string;
configuration?: string;
} {
const [project, target, configuration] = splitByColons(taskId);
if (!project || !target) {
throw new Error(
`Invalid taskId "${taskId}" — expected "project:target[:configuration]"`
);
}
return { project, target, configuration };
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nope, we have dedicated utils for this stuff

Comment on lines +88 to +99
// The result key is usually the same as taskId but may include a
// defaultConfiguration suffix when none was explicitly given.
let inputs: HashInputs | undefined = planResult[taskId];
if (!inputs) {
const prefix = `${project}:${target}`;
for (const [key, val] of Object.entries(planResult)) {
if (key === prefix || key.startsWith(prefix + ':')) {
inputs = val;
break;
}
}
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can read the target's default configuration instead of guessing here

Comment on lines +153 to +161
function findCanonicalTaskId(taskId: string, tg: TaskGraph): string | null {
if (tg.tasks[taskId]) return taskId;
const { project, target } = parseTaskId(taskId);
const prefix = `${project}:${target}`;
for (const id of Object.keys(tg.tasks)) {
if (id === prefix || id.startsWith(prefix + ':')) return id;
}
return null;
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is kinda crazy, we've got better utils for this. Don't duplicate.

Comment on lines +163 to +186
function getDepsOutputs(taskId: string): ExpandedDepsOutput[] {
if (depsOutputsCache.has(taskId)) return depsOutputsCache.get(taskId)!;

const tg = getTaskGraphFor(taskId);
if (!tg) {
depsOutputsCache.set(taskId, []);
return [];
}
const canonical = findCanonicalTaskId(taskId, tg);
if (!canonical) {
depsOutputsCache.set(taskId, []);
return [];
}
const task = tg.tasks[canonical] as Task;
let result: ExpandedDepsOutput[] = [];
try {
result =
getInputs(task, options.projectGraph, getNxJson()).depsOutputs ?? [];
} catch {
result = [];
}
depsOutputsCache.set(taskId, result);
return result;
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Simplify

Comment on lines +188 to +204
function getUpstreamTaskIds(taskId: string, transitive: boolean): string[] {
const tg = getTaskGraphFor(taskId);
if (!tg) return [];
const canonical = findCanonicalTaskId(taskId, tg);
if (!canonical) return [];
const direct = tg.dependencies[canonical] ?? [];
if (!transitive) return [...direct];
const visited = new Set<string>();
const queue = [...direct];
while (queue.length) {
const id = queue.shift()!;
if (visited.has(id)) continue;
visited.add(id);
queue.push(...(tg.dependencies[id] ?? []));
}
return [...visited];
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We almost certainly have a better util

Comment on lines +206 to +216
function pathMatchesOutputPattern(
normalizedPath: string,
pattern: string
): boolean {
const np = pattern.replace(/\\/g, '/');
return (
normalizedPath === np ||
normalizedPath.startsWith(np + '/') ||
minimatch(normalizedPath, np, { dot: true })
);
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Existing utils?

Comment on lines +218 to +223
function isOutputImpl(taskId: string, path: string): boolean {
const normalized = path.replace(/\\/g, '/');
return getOutputs(taskId).some((p) =>
pathMatchesOutputPattern(normalized, p)
);
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why these weird Impl suffixes? Also, why are we even doing the filtering like this? This sucks. Getting bespoke listts of outputs per path instead of some bulk query

const depsOutputs = getDepsOutputs(taskId);
if (depsOutputs.length === 0) return false;
for (const { dependentTasksOutputFiles, transitive } of depsOutputs) {
if (!minimatch(normalized, dependentTasksOutputFiles, { dot: true })) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We almost certainly have a util

@AgentEnder AgentEnder force-pushed the feat/check-sandbox-report branch from 31a7d28 to adae585 Compare May 11, 2026 21:13
nx-cloud[bot]

This comment was marked as outdated.

nx-cloud[bot]

This comment was marked as outdated.

nx-cloud[bot]

This comment was marked as outdated.

nx-cloud[bot]

This comment was marked as outdated.

@AgentEnder AgentEnder force-pushed the feat/check-sandbox-report branch 2 times, most recently from a6b9fe5 to 989f262 Compare June 11, 2026 15:57
nx-cloud[bot]

This comment was marked as outdated.

nx-cloud[bot]

This comment was marked as outdated.

@AgentEnder AgentEnder force-pushed the feat/check-sandbox-report branch from 7387ad4 to 54d2fe3 Compare June 15, 2026 18:56
@AgentEnder AgentEnder self-requested a review June 18, 2026 20:14
const matched: string[] = [];
const unmatched: string[] = [];
for (const file of files) {
if (isInput(taskId, file, ctx)) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pass more information into this function.. it should.. maybe just get all the files?


function isInput(taskId: string, path: string, ctx: LoadedContext): boolean {
const normalized = normalizePath(path);
const raw = getRawInputs(taskId, ctx);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should be done once per task

if (raw.files.includes(normalized)) return true;
if (raw.depOutputs.includes(normalized)) return true;
}
return matchesDependentTaskOutputs(taskId, normalized, ctx);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do we need this if we already checked if raw.depOutputs has it?

const matched: string[] = [];
const unmatched: string[] = [];
for (const file of files) {
if (isOutput(taskId, file, ctx.projectGraph)) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same as how isInputs is used.. this should probably receive all the files

resolvePrompt,
PromptResolutionError,
} from './command-line/migrate/prompt-files';
export { HashPlanInspector } from './hasher/hash-plan-inspector';

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We don't need to export this.

@AgentEnder AgentEnder force-pushed the feat/check-sandbox-report branch from 54d2fe3 to 5d8d533 Compare June 22, 2026 19:00
nx-cloud[bot]

This comment was marked as outdated.

nx-cloud[bot]

This comment was marked as outdated.

nx-cloud[bot]

This comment was marked as outdated.

nx-cloud[bot]

This comment was marked as outdated.

nx-cloud[bot]

This comment was marked as outdated.

nx-cloud[bot]

This comment was marked as outdated.

@AgentEnder AgentEnder marked this pull request as ready for review July 2, 2026 20:11
@AgentEnder AgentEnder requested a review from a team as a code owner July 2, 2026 20:11
@AgentEnder AgentEnder force-pushed the feat/check-sandbox-report branch from 29e9034 to 80390cc Compare July 7, 2026 16:09
nx-cloud[bot]

This comment was marked as outdated.

nx-cloud[bot]

This comment was marked as outdated.

nx-cloud[bot]

This comment was marked as outdated.

AgentEnder and others added 13 commits July 7, 2026 14:08
…cking

Adds a programmatic API that lets external consumers verify whether
sandbox-violation file paths from a prior task run would be considered
legitimate inputs/outputs in the current workspace configuration.

The API mirrors the logic that powers nx show target --check:
inputs are reconciled via HashPlanInspector.inspectTaskInputs, outputs
via getOutputsForTargetAndConfiguration with glob matching.

HashPlanInspector and the new verifier are now exported from
devkit-exports for use by the nx-cloud light client.
…evkit-internals

Conflict resolution: added PR exports (HashPlanInspector, TaskFileResolver,
createTaskFileResolver) after master's new resolveImplementation/resolvePrompt
exports in devkit-internals.ts.
…ally

Previously isInput() only matched against the materialized HashInputs.files
list — when an upstream task hadn't run yet, depOutputs was empty and any
file declared via { dependentTasksOutputFiles: '...', transitive?: bool }
was reported as not-an-input even when the path obviously matched both the
glob and a declared upstream output.

The new logic walks the task graph from the inspected task, pulls each
upstream's declared output globs, and reports the path as an input when
it matches the dependentTasksOutputFiles glob AND lies inside one of those
upstream outputs. Honors transitive: true/false. The check is exposed
separately as resolver.matchesDependentTaskOutputs so consumers (e.g. the
nx-cloud check-sandbox-report command) can reason about why a path was
considered an input.

Adds 6 unit tests covering: materialized depOutputs, static glob+output
match, glob-without-output mismatch, output-without-glob mismatch,
transitive=true walk, transitive=false short-walk, and the standalone
matchesDependentTaskOutputs accessor.
…uts public functions

Conflict resolution: removed TaskFileResolver/createTaskFileResolver exports from
devkit-internals.ts (between master's new resolveImplementation/resolvePrompt
exports and the PR's HashPlanInspector export).
Replaces splitByColons (low-level lexer) with the project-graph-aware
splitTarget helper so taskIds whose project name contains colons
(e.g. some:scoped:project:build) parse correctly.
Output patterns were evaluated one at a time, so a negated pattern like
'!{projectRoot}/.next/cache/**' acted as a standalone minimatch matcher
(matching everything outside the cache dir) instead of excluding files
from the positive patterns beside it.

- isOutput now uses whole-list semantics: a path matches when it matches
  a positive pattern and no negated pattern, mirroring the task runner's
  expand_outputs behavior
- also fixes 'nx show target outputs --check' and upstream-output
  matching for dependentTasksOutputFiles, which share this matcher
…ts --check

'nx show target inputs <task> --check <file>' only consulted HashInputs
(resolved files + materialized depOutputs), so a dependent task's output
was reported as 'not an input' until the upstream task had actually run.

- the --check path now also consults checkFilesAreInputs, which
  statically matches dependentTasksOutputFiles globs against upstream
  tasks' declared outputs via the task graph
- static matches are reported under the depOutputs category
check-task-files matched output patterns with minimatch plus hand-rolled
directory-containment and negation logic, which can drift from the
globset-based engine the task runner uses when expanding and hashing
outputs. Matching is now equivalent by construction:

- add matchOutputPaths: static path-vs-output-patterns matching that
  mirrors expand_outputs semantics (non-glob entries match themselves
  and anything nested under them, negated entries exclude from the
  whole set, a list with no positive entries matches nothing) without
  touching the file system
- add matchGlobPaths: build_glob_set matching for arbitrary globs,
  used for dependentTasksOutputFiles the same way hash_task_output
  filters dep outputs
- check-task-files now delegates all pattern matching to these,
  dropping its minimatch-based matcher
matchOutputPaths returned no matches when the pattern list had no
positive entries, diverging from expand_outputs, which keeps everything
the negated globs do not exclude. Static matching now reproduces that
fallback so --check answers exactly what the cache would capture; an
empty list still matches nothing, as expand_outputs returns nothing for
it as well.
@AgentEnder AgentEnder force-pushed the feat/check-sandbox-report branch from 75653f5 to 31c9117 Compare July 7, 2026 18:11
AgentEnder and others added 2 commits July 7, 2026 15:16
'coerce' was imported as a named value from 'yargs' but never used as a
value — every 'coerce:' in this file is an object-literal key. The
named import only typechecked as a property lookup on the Argv object
that @types/yargs exports via 'export =', and ts-jest's nodenext
handling rejects that form with TS2614.

This was latent until this PR: the new devkit re-exports
(checkFilesAreInputs/Outputs, HashPlanInspector) connect @nx/devkit to
command-line-utils and shared-options, pulling this file into the e2e
ts-jest type-check set for the first time and failing every e2e-ci
task at transform time. Argv and ParserConfigurationOptions are only
used as types, so the import becomes type-only.

@nx-cloud nx-cloud Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nx Cloud has identified a flaky task in your failed CI:

🔂 Since the failure was identified as flaky, we triggered a CI rerun by adding an empty commit to this branch.

Nx Cloud View detailed reasoning in Nx Cloud ↗


🎓 Learn more about Self-Healing CI on nx.dev

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants