feat!: Bump openjd-* Rust crates to the 0.10.0 release - #373
Merged
leongdl merged 2 commits intoSep 28, 2026
Merged
Conversation
openjd-rs released on 2026-09-28: openjd-expr 0.10.0, openjd-model 0.10.0, openjd-sessions 0.7.1 (OpenJobDescription/openjd-rs#408). This package pinned 0.9.0 / 0.9.0 / 0.7.0. openjd-sessions 0.7.1 is a transitive re-pin with no source change. Two breaking changes, both features: - #409 makes template::AmountRequirement::name and template::AttributeRequirement::name a FormatString instead of a String, so a capability name may contain expressions (openjd-specifications#189). The §3.3.1.1 / §3.3.2.1 constraints now apply to the resolved name. - #407 requires create_job's context to cover the template's declared extensions. With mismatch impossible, the job-creation resolved-value checks report every evaluation error instead of silently skipping it. Only #409 broke compilation, in four places: the two template-type constructors and the two name getters. The Python-facing `name` stays a `str` holding the raw template text. v0 models these as AmountCapabilityName / AttributeCapabilityName, both subclasses of v0's FormatString, which subclasses str -- so exposing an openjd.expr .FormatString here would make v0 and v1 diverge where they agree, and .raw() is what #409 prescribes for reading these fields. One behaviour change falls out of the adaptation rather than upstream: the constructor parses, so a malformed format string now raises ExpressionError. create_job needed no code change. The binding derives its context from job_template.default_validation_context() when the caller passes none, which covers the template's extensions by construction; a caller-supplied mismatched context is what #407 now rejects, and the binding surfaces it. #407 is a squash of five commits whose message documents three behaviour changes the release changelog and the PR body do not name: - eval_boolop no longer suppresses budget errors in operands after an unresolved one, the same bypass eval_ifexp had. - A list comprehension over a *concrete* iterable whose filter evaluates unresolved concludes unresolved[list[T]] instead of hard-erroring. This was a defect that predated the release, masked by the lenient policy #407 deleted, and reachable only at job creation -- the one stage where the iterable is concrete and the filter is not. - Template validation and create_job's re-checks evaluate under PathFormat::Posix, so validation outcomes no longer depend on the host OS. Windows-only in effect; unverified locally. Verified: 6243 passed / 24 skipped / 3 xfailed, coverage 94.22%. The three xfails are the pre-existing openjd.expr known gaps; none flipped. ruff, black, mypy, cargo fmt and clippy clean. Every behaviour change above was measured through this package's public API against both 0.9.0 and 0.10.0, and the openjd.expr expectations are taken from the upstream assertions in test_unresolved_eval.rs and test_memory.rs. Four mutants, each rebuilt and each caught: reverting the three pins kills 53 of the new cases, the two __repr__ sites kill 2, the two name getters kill 11, and swallowing the name parse error kills 2. Each was restored byte-for-byte with the bytecode cache cleared between runs. THIRD-PARTY-LICENSES.txt moves only the three version lines; the release pulled in no new transitive crates. Signed-off-by: David Leong <116610336+leongdl@users.noreply.github.com>
AlexTranAmz
previously approved these changes
Sep 28, 2026
jericht
previously approved these changes
Sep 28, 2026
mwiebe
reviewed
Sep 28, 2026
Review feedback on OpenJobDescription#373: consistency with the other v1 fields beats consistency with v0 here. openjd-rs#409 made template::AmountRequirement::name and template::AttributeRequirement::name a FormatString. The first pass kept the Python-facing `name` a `str` and parsed on the way in, on the grounds that v0 models these as AmountCapabilityName / AttributeCapabilityName, both subclasses of v0's FormatString, which subclasses str. That argument looks at the wrong neighbour: every other FormatString-typed field on the v1 template types -- `min`, `max`, `anyOf`, `allOf`, `Action.command` -- is an openjd.expr.FormatString and refuses a bare str. `name` now matches them. This deletes more than it adds. parse_capability_name and its doc comment go away, because the FormatString arrives already parsed, and both constructors go back to being infallible. Validation moves to where it belongs: a malformed name is rejected by FormatString's own constructor rather than by a str-typed field that parses behind the caller's back. __repr__ keeps .raw(), matching Action's treatment of its command. This is a breaking change to the v1 Python API, and two pre-existing tests prove it: TestPickle::test_amount_requirement and test_attribute_requirement both passed `name=` a str, and TestStepTemplate::test_host_requirements compared `.name` to one. All three are updated, so this is a behaviour change and not a refactor. Also from review: the spec edit that changed the documented ModelProfile repr to `revision=v2023_09` was wrong -- PyModelProfile::__repr__ still emits the uppercase-V form (rust-bindings/src/model/profile.rs:397), measured as `ModelProfile(revision=V2023_09, extensions=[EXPR])`. Reverted. The two `SpecificationRevision.v2023_09` corrections in the same block stand: that repr is lowercase (profile.rs:63) and `V2023_09` is not an attribute at all. The divergence between the two reprs is pre-existing and left alone here. Verified: 6245 passed / 24 skipped / 3 xfailed, coverage 94.32%. ruff, black, mypy, cargo fmt and clippy clean. Four mutants, each rebuilt and each caught: reverting the pins to 0.9.0 kills 68 cases, formatting the FormatString in both __repr__ sites kills 2, both getters returning a constant kills 12, and both constructors discarding the supplied name kills 10. Signed-off-by: David Leong <116610336+leongdl@users.noreply.github.com>
mwiebe
approved these changes
Sep 28, 2026
AlexTranAmz
approved these changes
Sep 28, 2026
Merged
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes: n/a (dependency bump for OpenJobDescription/openjd-rs#408)
What was the problem/requirement? (What/Why)
openjd-rs released on 2026-09-28: openjd-expr 0.10.0, openjd-model 0.10.0, openjd-sessions 0.7.1. This package pinned 0.9.0 / 0.9.0 / 0.7.0. openjd-sessions 0.7.1 is a transitive re-pin with no source change.
The release is two features, both breaking:
template::AmountRequirement::nameandtemplate::AttributeRequirement::namebecomeFormatStringinstead ofString, so a capability name may contain expressions (openjd-specifications#189). The §3.3.1.1 / §3.3.2.1 constraints apply to the resolved name.create_job's context must cover the template's declared extensions. With mismatch impossible, the job-creation resolved-value checks report every evaluation error instead of silently skipping it.Only #409 broke compilation, in four places: the two template-type constructors and the two
namegetters.#407 is a squash of five commits, and its commit message documents three behaviour changes that neither the release changelog nor the PR body names. I found them by diffing the published crate sources tag to tag and reading the commit messages behind each changed file:
eval_boolopno longer suppresses budget errors in operands after an unresolved one — the same bypasseval_ifexphadunresolved[list[T]]instead of hard-erroringcreate_job's re-checks evaluate underPathFormat::Posix, so validation outcomes no longer depend on the host OS407-e is the one that matters. It is a defect that predated the release, masked by the lenient error policy #407 deleted, and reachable only at job creation — the one stage where the iterable is concrete and the filter is not. A template like
passed
openjd check(iterable unresolved, the tolerant path), ran cleanly on workers (everything bound), and was rejected atcreate_job.What was the solution? (How)
Bump the three pins, adapt the four call sites, and pin every behaviour change that reaches this package's public API.
The Python-facing
namebecomes anopenjd.expr.FormatStringtoo, matchingmin/max/anyOf/allOfandAction.command— every other FormatString-typed field on the v1 template types, all of which already refused a barestr. Reading the template text needs.raw(); constructing one needsFormatString(...).The first revision of this PR kept
nameastrand parsed on the way in, on the grounds that v0 models these asAmountCapabilityName/AttributeCapabilityName, both subclasses of v0'sFormatString, which subclassesstr. @mwiebe pointed out that this looks at the wrong neighbour: consistency with the sibling v1 fields matters more than consistency with v0. He is right, and the change is a net deletion —parse_capability_nameand its doc comment go away because theFormatStringarrives already parsed, and both constructors go back to infallible. Validation lands where it belongs: a malformed name is rejected byFormatString's own constructor rather than by astr-typed field parsing behind the caller's back.The job-side
AmountRequirement.name/AttributeRequirement.namestaystr:job::AmountRequirement::nameis still aStringupstream, because it holds the resolved name.create_jobneeded no code change. The binding derives its context fromjob_template.default_validation_context()when the caller passes none, which covers the template's extensions by construction. A caller-supplied mismatched context is exactly what #407 now rejects, and the binding surfaces it asModelValidationError.What is the impact of this change?
Behaviour changes, each measured through this package's API on both 0.9.0 and 0.10.0.
openjd-rs#409, through
openjd.model._v1:decode_job_template,amounts[0].name = "{{Param.Attr}}"name '{{Param.Attr}}' does not match capability name pattern..nameis the raw textattributes[0].namebogus.name{{ 'bogus.static' }}-> nameexceeds 100 characters."amount.custom.<95 chars>{{Param.Attr}}"resolves to at least 109 characters, exceeding the maximum of 100.duplicate amount name 'amount.custom.x'.anyOf: [plan9]value 'plan9' is not valid for attr.worker.os.family.allOfsingle-valued attribute cannot have more than 1 element.{{Task.Param.F}}in a nameUndefined variable: 'Task.Param.F'with caret — a name resolves at job creation, where task parameters are not boundcreate_job, resolvedamount.custom.x/attr.custom.xcreate_job, resolvednot a namedoes not match capability name pattern.create_job, resolved 101 charactersexceeds 100 characters.(100 accepted)create_job, resolvedamount.worker.made_upuses reserved scope 'worker'.create_job, resolvedAMOUNT.CUSTOM.Xvs literalamount.custom.xduplicate amount name 'AMOUNT.CUSTOM.X'.create_job, resolvedattr.worker.os.familywithanyOf: [plan9]openjd-rs#407:
create_jobwith a context stripping the template'sEXPRcreate_job requires a context enabling every extension the template declares: missing EXPR.args[0] = "{{ 10 // Param.N }}",N=0Division by zerowith caret atsteps[0] -> script -> actions -> onRun -> args[0]'A' * 100000 if Session.Flag else 'B', 5-operation budgetunresolved[string]operation count (394) exceeded limit (5)unresolved[string]memory usage (100136 bytes) exceeded limit (1024 bytes)create_jobwithCallerLimits(max_eval_operations=5)operation count (395) exceeded limit (5)Session.Flag and len('A' * 100000) > 0, 5-operation budgetunresolved[bool]operation count (396) exceeded limit (5)[f for f in 'a,b,c'.split(',') if f != Task.Skip]List comprehension filter must be a boolean, got unresolved[bool]unresolved[list[string]][10 // x for x in [0, 2] if x > Task.N]unresolved[list[int]]— the body is typed under an unresolved loop variable, so an element the run-time filter may exclude cannot raiseIn every budget case a value error in the same position is still absorbed, because a run-time short-circuit may never reach it. A comprehension filter whose type can never be a boolean is still an error. Both are pinned as negative controls.
Public contract — this breaks the v1 Python API in one field.
TemplateAmountRequirement.nameandTemplateAttributeRequirement.namechange fromstrtoopenjd.expr.FormatString, in both directions. A caller reading.nameneeds.raw(); a caller constructing one needsFormatString(...). The.pyistub is updated to match.Three pre-existing tests are the evidence that this is a behaviour change and not a refactor, and all three are updated:
TestPickle::test_amount_requirementandtest_attribute_requirementboth passedname=astr, andTestStepTemplate::test_host_requirementscompared.nameto one.Two other behaviour changes reach existing callers. A template with a format-string capability name now decodes where it used to be rejected, which is the point of #409. And a malformed name is now rejected — at
FormatStringconstruction rather than at the requirement's.Cargo.lockmoves only the three crates.THIRD-PARTY-LICENSES.txtwas regenerated withscripts/check_third_party_licenses.sh --updateand changed only the three version lines: the release pulled in no new transitive crates.How was this change tested?
hatch run test: 6245 passed, 24 skipped, 3 xfailed, coverage 94.32%. The three xfails are the pre-existingopenjd.exprknown gaps; none flipped, so nothing in this release closed them.hatch run lint(ruff, black, mypy) clean.cargo fmt --all --checkandcargo clippy -p openjd-python --all-targets -- -D warningsclean.The
openjd.exprexpectations are copied from the upstream assertions incrates/openjd-expr/tests/integration/test_unresolved_eval.rs, not from its prose — all five comprehension cases agree.New tests, each with a negative control:
test_parse.py:TestFormatStringCapabilityNames(decode acceptance, the pattern check, the expression-error path),TestFormatStringCapabilityNameChecksAtValidation(empty and over-length static names,let-bound static names, the partly-static lower bound, static-vs-literal uniqueness, single-reporting of a literal duplicate, the standard-attribute value and single-valued-allOfrules, a job-creation-unavailable symbol, and the deferral control)test_create_job.py:TestFormatStringCapabilityNamesAtJobCreation(every rule parametrized over bothamountsandattributes, plus the 100/101-character boundary and the resolved name selecting the standard-capability value rules),TestCreateJobContextExtensionContract(all four context shapes),TestValueDependentEvaluationErrorAtJobCreation,TestEvaluationBudgetsInsideUnresolvedConditionals,TestUnresolvedFilterComprehensionAtJobCreation,TestValidationIsIndependentOfTheHostPathFormattest/openjd/expr/test_unresolved_eval.py:TestConcreteIterableUnresolvedFilter,TestBoolOpBudgetErrorsPropagateandTestIfExpBudgetErrorsPropagate(both parametrized over both arms —and/or, if-branch/else-branch — plus the composition of the two exemptions)test_template_types.py:TestCapabilityNameIsAFormatString(theFormatStringround trip, the refusal of a barestr, the malformed-name rejection,__repr__stability, pickle)Mutation check — four mutants, each rebuilt from source and each caught:
__repr__sites format theFormatStringinstead of.raw()namegetters return a constantEach mutant was restored byte-for-byte (checksum-verified) with the bytecode cache cleared between runs. The tests that survive the pin revert are the deliberate negative controls; a separate liveness mutant that flipped each control's expected value failed all of them.
Three independent auditors reviewed the tests (does each pin the behaviour, what does it fail to cover, craft and conventions). The gap auditor's first pass returned a fail with ten uncovered behaviours — the whole attribute half of #409 at job creation, the decode-time static-name checks, the
orarm of 407-d and the else-branch of 407-c, and the partly-static lower bound. All were confirmed reachable by probe and are now covered; the tables above are the post-fix state.What could NOT be verified
407-g, the POSIX path-format change, is Windows-only in effect. On a POSIX host the host format is POSIX, so there is nothing to observe on macOS or Linux. On 0.9.0 a PATH value flowing from a
letbinding into an argument drewPath format mismatchon Windows — 11 conformance failures upstream.TestValidationIsIndependentOfTheHostPathFormatpins the construction and is a no-op assertion locally; it earns its keep on the Windows CI lane only, and its docstring says so.407-f,
SymbolTable::setfailures in check-symtab seeding now propagating asModelErrorrather than degrading the check to a no-op, is unreachable from here by construction. Upstream's commit message states the seed keys are uppercase-rooted while let-binding names must start lowercase, so a collision requires an internal invariant to already be broken. No test added.TestUnresolvedFilterComprehensionAtJobCreationdoes not discriminate 0.9.0 from 0.10.0. The template creates a job on both, for different reasons: 0.9.0's lenient policy silently skipped the error, 0.10.0 raises none. It is there because the two halves of the release have to hold together — #407's strict policy without its companion comprehension fix rejects that template — but it is not evidence that 407-e landed. Theopenjd.exprcases are. Its docstring says this.Was this change documented?
Yes.
specs/python-model-interface.md— a table of which stage applies the capability-name constraints for a literal, fully static, partly static and parameter-dependent name; the symbol scope available to a name; theFormatStringtype of the template-sidenameagainst thestrof the job-side one; thecreate_jobcontext contract with its error message, replacing text that described passing a divergent profile as supported policy; the strict evaluation-error policy; and the OS-independence of validation outcomes. Also correctedModelProfile.from_strings(SpecificationRevision.V2023_09, ...)tov2023_09in the same example —V2023_09is not an attribute, so that line raisedAttributeErroras written.specs/python-expr-interface.md— the three unresolved-propagation rules that changed, stated as rules rather than as release notes.src/openjd/_openjd_rs.pyi— thenametype on both classes, with a docstring pointing at.raw(). Hand-edited;scripts/generate_stubs.shdoes not run on macOS.namegetters inrust-bindings/src/model/template_types.rs.Is this a breaking change?
Yes, in one field.
TemplateAmountRequirement.nameandTemplateAttributeRequirement.nameareopenjd.expr.FormatStringinstead ofstr. A caller reading the template text uses.raw(); a caller constructing one wraps inFormatString(...). The job-side equivalents are unchanged.Nothing else is removed or narrowed. See the impact section for the two non-breaking behaviour changes: a format-string capability name now decodes, and a malformed one is now rejected.
Does this change impact security?
Indirectly, in the direction of enforcement. Two ways a caller's lowered evaluation budget silently stopped applying are closed — inside a conditional whose test only a worker can resolve, and in an
and/oroperand after an unresolved one. Both are the idiomatic constructions for a worker-resolved value, so the bypass was reachable rather than theoretical. Separately, a value-dependent evaluation failure now fails submission instead of every session that runs the job.By submitting this pull request, I confirm that you can use, modify, copy, and redistribute this contribution, under the terms of your choice.