Inspect writes a work queue, not a recipe

Questions

  • What two files does package inspect write, and which claim do they establish?

  • When is a leftover a residual, and when is it only a parser note?

  • Why does package plan exit without --stack-policy?

What you will be able to do

  1. Run package inspect on a foreign recipe and name every section of package.plan.json that is evidence, and every residual that is work.

  2. Tell a mechanical residual from a judgment residual from a blocking one, and say which stage each belongs to.

  3. Write the package plan command that turns that inspect tree into a lock and a .eb, including the stack policy the CLI requires.

Before this chapter

eb-stack ports a recipe and bumps one placed the conda-forge recipe beside the .eb and halted at the residual queue. package inspect is the command executable from a fresh clone lacking EasyBuild and a robot tree. Its output constitutes the remainder of this chapter. The fixture comprises the rattler-build v1 recipe for eOn, captured from the conda-forge feedstock:

eb-stack package inspect \
  --source fixtures/foreign_ingest/conda_eon/recipe.yaml \
  --format conda-forge \
  --toolchain-name foss \
  --toolchain-version 2026.1 \
  --package-config examples/packages/common.toml \
  --package-config examples/packages/eon.toml \
  --out-dir /tmp/eon-inspect

Two files come out, and nothing else:

/tmp/eon-inspect/package.plan.json
/tmp/eon-inspect/package.sbom.cdx.json

There is no locks/ directory and no easyconfigs/. Inspection does not call Resolvo. It establishes no rung of the ladder.

The plan is a manifest with provenance

package.plan.json has schema_version 1. Unknown fields are rejected. The sections that matter on a first read:

Field

What it records

origin

conda-forge, spack, pypi, cran, cargo, or easy-build

package

normalised name, version, homepage, license

sources

URLs, filenames, SHA-256, VCS identity

dependencies

names, constraints, roles, conditions, provenance

build

toolchain, easyblock, patches, typed easyconfig parameters

profiles

independently installable products

outputs

requested profile and stack-policy pairs

residuals

the work queue

Provenance on a source or a dependency is exact, derived, or ambiguous, plus a span. Written paths are identities (conda-forge/recipe.yaml, spack/package.py), not checkout locations, so two hosts produce the same JSON from the same input. A conda build.number is a parser note. A compiler or cross-python template that the EasyBuild toolchain already represents is also a parser note. Notes are diagnostics. Planning never greps them for the word “residual”. Anything that needs a human or a later stage is a typed residual instead.

Residuals are the work queue

Each residual includes id, stage, category, severity, summary, and may also contain optional evidence and provenance.

{
  "id": "foreign:imperative-patch:0",
  "stage": "parse",
  "category": "imperative-patch",
  "severity": "judgment",
  "summary": "imperative patch method requires EasyBuild translation",
  "provenance": {
    "span": {"path": "package.py", "start_line": 42}
  }
}

Severity has three values and they constitute the triage.

  • mechanical: deterministic formatting or metadata.

  • recipe format can close some of these.

  • judgment: package-authoring or target evidence is required.

  • A Spack def patch(self): method that is not a file is this.

  • blocking: the next pipeline stage cannot make a valid claim.

  • A missing SHA-256 on a version bump without --source-checksum is this.

Stage is parse, normalization, resolution, emission, build, or verification. An inspect residual is almost always parse. A bump that changes the package version and does not pass --source-checksum adds a residual. The foreign-ingest README is explicit about the limit; these fixtures drive parser regression. They do not claim parity with a hand-authored EasyBuild pull request. Product flags, generation pins, and multi-source extract layout remain residual. That sentence is why inspect exists as a command of its own. The work queue is visible before Resolvo spends a robot tree on a recipe that still needs judgment. The planned SBOM, package.sbom.cdx.json, is CycloneDX 1.5 derived from the same plan. It is a pre-build inventory. It carries no generation timestamp, so two inspect runs of the same input hash the same. It does not establish builds or binary-verified.

EB-Residual-1 — Note, residual, or claim

Four inspect outputs. For each, say whether it is a parser note, a typed residual (and which severity), or a claim.

  1. A conda build.number of 3.

  2. A Spack def patch(self): that applies a sed at build time.

  3. A source URL with a SHA-256 taken from the feedstock.

  4. package.sbom.cdx.json listing the same source.

Solution
  1. A parser note. The EasyBuild toolchain represents the rebuild counter; nothing in the next stage has to act on it.

  2. A judgment residual, category imperative-patch, stage parse. There is no patch file to copy beside the emitted .eb. Someone has to write one, or drop the change.

  3. Evidence in sources. Not a residual. Provenance is exact.

  4. The planned SBOM. Not a claim. The same inventory in a different schema. resolves still has not been established.

Plan is inspect plus Resolvo, and it needs a policy

The command that defines resolves is package plan. The current CLI needs --stack-policy. eb-stack ports a recipe and bumps one’s example left it out. The live form from the tutorial is:

eb-stack package plan \
  --source fixtures/foreign_ingest/conda_eon/recipe.yaml \
  --format conda-forge \
  --toolchain-name foss \
  --toolchain-version 2026.1 \
  --package-config examples/packages/common.toml \
  --package-config examples/packages/eon.toml \
  --easyconfigs "$ROBOT" \
  --easyconfigs /tmp/eb-stack/overlay \
  --stack-policy examples/stacks/eon-foss-2026.1.toml \
  --out-dir "$BUNDLE"

The generated tree is now:

package.plan.json
package.sbom.cdx.json
locks/default.lock.json
easyconfigs/e/eOn/eOn-2.16.0-foss-2026.1.eb

The lock corresponds to the Resolvo result. Direct dependencies specify selected versions, toolchains, versionsuffixes, source easyconfig paths, build/runtime roles, pin outcomes, and candidate exclusions. The stack-policy TOML is mandatory, not optional decoration. The eOn example records the Eigen 5 identity required by the safemath core-guard patch as a preferred pin with the complete EasyBuild toolchain and versionsuffix. Resolvo attempts that candidate first. If another declared requirement renders it infeasible, the solver selects a compatible alternative and sets fallback = true with a reason. locked is the mode that must instead fail. package bump of an existing .eb produces the same bundle. Without --stack-policy, the bump solver applies an unconstrained policy for the requested toolchain. plan of a foreign recipe does not receive that relaxation; the flag is required. When a direct dependency lacks a robot candidate, --package-catalog and --package-sources close the gap without inventing versions. A closed plan writes companions under packages/, a shared easyconfigs/ overlay, build-order.json, and closure.sbom.cdx.json. Robot candidates continue to take precedence over catalog entries.

EB-Plan-1 — What is missing from this command

A colleague pastes eb-stack ports a recipe and bumps one’s plan sample:

eb-stack package plan \
  --source recipe.yaml \
  --format conda-forge \
  --toolchain-name foss \
  --toolchain-version 2024a \
  --easyconfigs /path/to/easybuild-easyconfigs \
  --out-dir /tmp/new-package
  1. What action does the current CLI perform with this?

  2. Which file is added, and which aspect does a preferred pin in it control?

  3. After a successful plan, which two paths must exist before campaign run will start?

Solution
  1. It exits with an error. --stack-policy is required on package plan.

  2. A stack-policy TOML, for example examples/stacks/eon-foss-2026.1.toml. A preferred pin names the candidate Resolvo tries first and records fallback = true if it cannot keep it. It is not a lock until the solve writes locks/default.lock.json.

  3. At least one locks/<profile>.lock.json and one emitted .eb. Inspect’s two JSON files are not enough. The campaign reference says so: a campaign requires at least one lock and recipe.

What to remember

  • package inspect writes package.plan.json and a planned CycloneDX SBOM. It establishes no claim.

  • Residuals are the work queue. Notes are not. Severity is mechanical, judgment, or blocking.

  • package plan requires --stack-policy. The lock and the emitted .eb are what establish resolves.