Skip to content

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

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

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

Conversation

@polygraph-snapshot-app
Copy link
Copy Markdown
Contributor

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

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
Copy link
Copy Markdown

netlify Bot commented May 5, 2026

Deploy Preview for nx-docs ready!

Name Link
🔨 Latest commit 9307800
🔍 Latest deploy log https://app.netlify.com/projects/nx-docs/deploys/6a03f7c78232c100085095f1
😎 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
Copy link
Copy Markdown

netlify Bot commented May 5, 2026

Deploy Preview for nx-dev ready!

Name Link
🔨 Latest commit 9307800
🔍 Latest deploy log https://app.netlify.com/projects/nx-dev/deploys/6a03f7c71478de0008713ada
😎 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
Copy link
Copy Markdown
Contributor

nx-cloud Bot commented May 5, 2026

View your CI Pipeline Execution ↗ for commit 71a4d46

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

☁️ Nx Cloud last updated this comment at 2026-05-13 04:21:35 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

…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

Replaces the sandbox-report-aware verifySandboxViolations export with
a generic primitive. createTaskFileResolver returns a handle exposing
getInputs / getOutputs / isInput / isOutput per task — the cloud
light client owns the SandboxReport shape and the iteration loop.

Exposed via devkit-internals (not the public devkit-exports surface).

A follow-up commit will refactor nx show target --check to consume
this same resolver.
…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.
@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.

AgentEnder and others added 2 commits May 12, 2026 18:13
…uts public functions

Surfaces are simpler (no factory pattern): callers pass files directly and
receive { matched, unmatched } without constructing or threading a resolver
object.

Functions are public (devkit-exports) not internal: Cloud can import them
directly without going through requireNx / devkit-internals.

Module-level graph caching keeps it fast: createProjectGraphAsync and
HashPlanInspector.init are called at most once per process — the same
efficiency as the old resolver but without the explicit pass-through.

Breaking: TaskFileResolver interface and createTaskFileResolver factory
are removed from devkit-internals. Cloud should import checkFilesAreInputs
/ checkFilesAreOutputs from @nx/devkit instead.
…uts public functions [Self-Healing CI Rerun]
nx-cloud[bot]

This comment was marked as outdated.

AgentEnder and others added 2 commits May 12, 2026 23:16
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.
Copy link
Copy Markdown
Contributor

@nx-cloud nx-cloud Bot left a comment

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.

1 participant