Inspect writes a work queue, not a recipe

inspect writes residuals into a work queue, not an easyconfig

Inspect writes a work queue. It does not write a recipe.

inspect to residuals to a work queue

Inspect writes a work queue. It does not write a recipe.

inspect residuals, then plan, then rewrite

A residual is named before any rewrite. The plan is the list of those names.

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 page

The link 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 that lacks EasyBuild and a robot tree. Its output is the remainder of this page. 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

The locks/ directory is missing. The easyconfigs/ directory is missing. Resolvo is not called by Inspection. The lock and easyconfigs directories are still missing.

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 are listed below. These names belong to the plan, not to a conda-forge recipe.yaml. Inspecting a conda-forge recipe does not emit Spack residuals. The origin on each residual is the format that was passed in. A rattler-build recipe uses build.number and script; outputs is for split packages. origin records conda-forge, spack, pypi, cran, cargo, or easy-build. package records normalised name, version, homepage, license. sources records URLs, filenames, SHA-256, VCS identity. dependencies records names, constraints, roles, conditions, provenance. build records the planned EasyBuild toolchain, easyblock, patches, and typed easyconfig parameters. profiles records independently installable products. outputs records requested profile and stack-policy pairs. residuals records the work queue. Provenance on a source or a dependency is exact, derived, or ambiguous, plus a span. Written paths are identities such as conda-forge/recipe.yaml, spack/package.py. Written paths are not checkout locations. 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

The cards that follow are examples from both formats rather than one inspect of the eOn recipe.yaml. A conda-forge inspect of that fixture does not emit R2 or R4.

Residual

id R1 stage parse category missing-checksum severity blocking summary Version bump has no SHA-256 and –source-checksum was not passed.

Residual

id R2 stage parse category imperative-patch severity judgment summary Spack def patch(self) applies sed at build time; no patch file is beside the .eb.

Residual

id R3 stage parse category split-outputs severity judgment summary rattler-build outputs splits packages; EasyBuild has one prefix per easyconfig.

Residual

id R4 stage parse category range severity judgment summary Spack states a lower bound; the robot has not yet picked a pin this generation provides.

Residual

id R5 stage parse category multi-source-extract severity judgment summary Extract layout is not a single tarball.

Residual

{
 "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 for 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. Split outputs, generation pins, and multi-source extract layout remain residual. That sentence explains 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. 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. No patch file exists 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 defining resolves is package plan. The current CLI requires --stack-policy. The port listing on eb-stack ports a recipe and bumps one already passes it. 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. Optional decoration is not allowed. 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. Another declared requirement may render 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. The bump solver applies an unconstrained policy for the requested toolchain. This occurs without --stack-policy. plan of a foreign recipe does not receive that relaxation. The flag is required. A direct dependency may lack 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 happens when that command is run as written?

  2. Which extra file does the command need, and what does a preferred pin in it control?

  3. After a successful plan, which two artifacts must exist before campaign run starts?

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. The pin 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.