feat!: enforce resolved-value constraints at template validation - #383
Open
mwiebe wants to merge 15 commits into
Open
feat!: enforce resolved-value constraints at template validation#383mwiebe wants to merge 15 commits into
mwiebe wants to merge 15 commits into
Conversation
mwiebe
force-pushed
the
feat/group-a-resolved-value-validation
branch
from
September 11, 2026 19:17
b1be0fd to
863bf9b
Compare
Template Schemas mandates target types for single whole-field
expressions in three action fields: `int?` for timeout (§5) and
notifyPeriodInSeconds (§5.3.2), `string?` for a deferred cancelation
mode. The runtime resolved all three untargeted and hand-matched the
value, deviating from the spec: `timeout: "{{ 120.0 }}"` failed with
"must be a positive integer, got Float" where the mandated coercion
accepts it as 120.
resolve_action_timeout, resolve_notify_period_seconds, and
resolve_effective_cancelation now pass the mandated targets via
with_target_type. The String match arms remain for multi-segment format
strings (which concatenate regardless of target) and now trim before
parsing, matching the reference implementation's int() leniency.
BREAKING CHANGE: whole-field expressions in `timeout` and
`notifyPeriodInSeconds` now coerce under `int?` — whole-number floats
and numeric strings are accepted, and uncoercible values fail at
resolution with coercion diagnostics instead of hand-written messages.
Signed-off-by: Mark <399551+mwiebe@users.noreply.github.com>
Expression Language §1.3.2 ("Evaluation Within Template Schemas") gives
every format-string field a target type for its whole-field
expressions: `T` for a required field, `T?` for an optional one (null =
field omitted). Job creation and the session runtime resolved the
fields with spec-mandated resolved-value constraints untargeted
(resolve_string_with, then trim-and-parse), which silently rendered a
whole-field null as the empty string and a list value as its display
form.
Adopt the schema-derived targets at resolution time:
- job `name`, task-parameter STRING/PATH range elements, and
hostRequirements attribute values resolve with target `string`
(create_job / resolve_string_range / resolve_string_list)
- hostRequirements amount `min`/`max` resolve with `float?` — a
whole-field null now means "field omitted" instead of a parse error
- chunks `defaultTaskCount` resolves with `int` and the optional
`targetRuntimeSeconds` with `int?`, so `{{ 4.0 }}` coerces to 4
- environment variable values resolve with `string` in the session
runtime
Multi-segment strings concatenate as before; numeric text parses with
surrounding whitespace tolerated, matching the reference
implementation.
BREAKING CHANGE: a whole-field `null` or list-valued expression in a
required string field (job name, range elements, attribute values,
environment variable values) is now a resolution error; there is no
list→string or null→string coercion. A bare LIST[*] parameter reference
as a single range element previously rendered the list's display form
as one element and is now rejected. Amount `min`/`max` resolving to
`null` now means the bound is unset instead of erroring, and
non-numeric/non-finite amount strings fail with coercion diagnostics.
Signed-off-by: Mark <399551+mwiebe@users.noreply.github.com>
Gate 1 of the resolved-value limits design: template validation
(pass 8) no longer discards what static evaluation computes. Each field
whose spec constraints apply to the resolved value — "after the format
string has been resolved" in the spec's wording — carries a
ResolvedConstraint applied to the StaticResolution that
validate_expressions returns, in two stages:
1. Lower bound: min_resolved_string_len holds for every possible
run-time resolution (unresolved segments contribute 0), so a bound
past the field's limit fails `openjd check` without knowing the
unresolved parts: `"{{ Param.X }}{{ 'A' * 600 }}"` can never be a
valid job name.
2. Full value check: when the field is fully static, the same check job
creation or the worker would run on the resolved value runs at
`check` time with matching messages: a static notifyPeriodInSeconds
of 700 fails with "must not exceed 600."
Fields and limits: job name (128/512 chars + Cc check), attribute
capability values (100 chars / standard allowed sets, including a
longest-allowed-value bound for standard capabilities), STRING/PATH
range elements (1024), environment variable values (2048 — previously
enforced nowhere), timeout and notifyPeriodInSeconds (positive, ≤ 600),
deferred cancelation mode (enum + 21-char bound), chunks
defaultTaskCount/targetRuntimeSeconds (≥ 1 / ≥ 0; these format strings
were previously never validated in pass 8 at all), and amount min/max
(≥ 0 / > 0, finite). Numeric fields use a soft 100-character bound
(MAX_RESOLVED_NUMERIC_LEN): leading zeros preclude an exact maximum,
but an i64 needs at most 20 characters.
Each field validates with the same schema-derived target type gates 2/3
resolve it with (Expression Language §1.3.2), so validation accepts
exactly what resolution accepts. Literal fields are excluded — the
raw-text passes already check those.
47 tests cover, per field: a fully static over-limit value fails, a
partially-unresolved value whose bound alone exceeds the limit fails,
and under-limit / fully-unresolved controls pass. Full conformance
suite passes (1,133/1,133).
BREAKING CHANGE: templates whose format strings statically violate
resolved-value constraints — including whole-field null or list values
in required string fields — now fail template validation instead of
failing at job creation or on the worker (or, for environment variable
values and chunks fields, not failing at all).
Signed-off-by: Mark <399551+mwiebe@users.noreply.github.com>
Review feedback on the gate-1 range-element constraint found two issues: 1. Wrong limit field: the constraint used max_task_param_range_len (the maximum number of elements in a list-form range) where the per-element character limit is max_task_param_string_len. Both are 1024 today, so no behavior differed — but the moment either limit moves, gate 1 would validate elements against the count cap while job creation enforces the length cap. 2. Bytes vs characters: the spec states its string limits in characters (and the reference implementation's len() counts characters), but several checks compared byte lengths — falsely rejecting non-ASCII values (a 600-character/1,200-byte range element failed job creation). Converted to character counts: range elements and float-range text in job creation (and the error message that reported bytes), the resolved job-name check, attribute capability values (whose length diagnostic also fired before the charset error for non-ASCII), and the raw-literal checks for job/step/environment names, identifiers, filenames, and literal attribute values. Tests pin both gates: 'é' * 1024 (2,048 bytes) validates and 'é' * 1025 fails at check; a 600-character non-ASCII range element now survives job creation with its character length intact. Conformance suite passes 1,133/1,133. Signed-off-by: Mark <399551+mwiebe@users.noreply.github.com>
Review feedback: float? targeting made a new state reachable — an amount requirement whose only provided bound is a whole-field expression resolving to null ends up with both min and max unset after resolution. Decode's "must have at least one of min or max." check runs on field presence, so it passes; check_resolved_amount_bounds only re-applied the sign and min <= max rules; and the job carried a boundless amount requirement that matches every worker. Re-apply the at-least-one rule on the resolved values, alongside the sign checks that are re-applied there for the same reason. The message gains an "after resolution" suffix so an author who did provide the field understands why it fired. Tests: min-only and min+max whole-field nulls are rejected at job creation; a null min alongside a concrete max still resolves to a one-sided bound. Signed-off-by: Mark <399551+mwiebe@users.noreply.github.com>
Review feedback: the gate-1 bound for environment variable values hardcoded 2048, silently duplicating the value the raw-text pass reads from EffectiveLimits — the same divergence-on-change failure mode as the range-element limit fixed earlier: if the limit ever moves, the literal and interpolated forms of the same field would disagree. Introduce a dedicated EffectiveLimits::max_env_var_value_len (2048) rather than reusing max_description_len: the raw-text pass had been borrowing the §7.2 Description limit for §4.4.2 environment variable values, another values-coincidence that would break the moment either spec section moves independently. Both the raw-text check and the gate-1 resolved-value constraint now read the same field, threaded into validate_env_format_strings by its three callers (job environments, step environments, and environment templates). Signed-off-by: Mark <399551+mwiebe@users.noreply.github.com>
…:Int Review feedback: after every integer field adopted its schema-derived target type, all four Int constructions passed targeted: true, so the targeted: false arm of target_type() was unreachable and the stage-2 checker ignored the flag. Worse than dead weight: a gate-1/gate-2 target mismatch is exactly the failure mode this design guards against, so an unused untargeted knob was a footgun — a future field constructed with targeted: false would get an untargeted gate 1 against a targeted gate 2 and silently diverge. Remove the field and let target_type() derive int vs int? from nullable alone; it now returns ExprType rather than Option<ExprType>, since every constraint has a target. Also scrub the three doc comments that still described untargeted numeric handling the code no longer has (the enum-level scoping paragraph, the Int variant doc, and the stage-2 trim comment). Signed-off-by: Mark <399551+mwiebe@users.noreply.github.com>
Review feedback: the amount non-finite test had been loosened from an
exact assert_eq! to a substring check — against the repo's
error-message test standard — and in the process lost the only
coverage of resolve_to_f64's is_finite branch: under the float? target
all five "nan"/"inf" loop values fail during coercion and none reach
the parse path.
Restore exact full-message assertions on both amount failure tests
(the loop is now explicitly a coercion test), tighten the
LIST[*]-in-range rejection test to assert the full path and message,
and add the multi-segment case that actually reaches the finite check:
"1e{{Param.Exp}}" with Exp=999 concatenates to "1e999", which parses
to infinity.
Also restore the finite-check diagnostic to report the resolved text
("'1e999' is not a finite number") instead of the parsed f64 ("'inf'"),
which this PR had regressed — the text is what the author can act on.
The check moves into the parse arm, the only path that can produce a
non-finite value (a coerced Float64 excludes NaN and the infinities by
construction).
Signed-off-by: Mark <399551+mwiebe@users.noreply.github.com>
Review feedback: code comments, the test-suite header, and specs/model/validation.md referenced specs/resolved-value-limits.md — a design document that lives on an unmerged branch, not in the repo — and leaned on its "gate 1/2/3" vocabulary, which nothing in the repo defines. Use the terminology the spec already establishes instead: the three processing stages of Template Schemas §7.4 (template validation, job creation, task execution on the worker host). The invariant reads as "a field's validation-time target must always equal its resolution-time target", and the constraint machinery is described as making template validation the earliest stage to catch a violation that is already knowable. References to the missing document are repointed at the Spec-Mandated Resolved-Value Constraints section of specs/model/validation.md, which is self-contained; the open question about list-item string? | list[string] semantics now lives there too. Also describe the current state rather than the change that produced it: "Pass 8 uses the values that static evaluation can resolve" rather than "no longer discards", and the string-target consequences state what is an error and why rather than comparing against prior behavior. The change narrative lives in the commit history and PR description. Signed-off-by: Mark <399551+mwiebe@users.noreply.github.com>
Review feedback established that Expression Language §1.3.2's list-item
rule — "For list items (e.g., in `args`), the target type is
`T? | list[T]`" — applies to list items generally, with `args` as its
worked example, not its scope. Task-parameter STRING/PATH range
elements and hostRequirements attribute `anyOf`/`allOf` values are the
same list-of-format-strings shape, so their whole-field expressions
target `string? | list[string]`: a `null` result skips the element and
a list result flattens inline, one element per list member, mixable
with literal elements:
range: ["first", "{{ RawParam.Paths }}", "last"]
This replaces the plain-`string` targeting these fields briefly carried
on this branch (which rejected list values), and is complementary to —
not redundant with — the whole-field `<ListExpressionString>` range
form (Expression Language §1.3.12), which replaces an entire range with
one expression and cannot mix literals.
At template validation, the per-element constraints (§3.4.2's 1024
characters, §3.3.2.2's charset/allowed-set rules) apply to each element
of a fully static list, and the string-length bound only applies when
`StaticResolution::resolved_type` says the resolution is certainly a
string — a list-valued resolution distributes its characters across
elements, so the display-form length says nothing about any single
element.
Because null-skips can empty a list whose non-emptiness was checked on
field presence at decode, job creation re-checks after resolution
("has no elements after resolution") — the same shape as the amounts
both-bounds-null fix.
Spec references verified against the upstream wiki: Expression Language
§1.3.2 "Evaluation Within Template Schemas" and §1.3.12 "Task Parameter
Range Field Extensions"; Template Schemas §3.4.2 TaskParameterStringValue,
§3.3.2.2 AttributeCapabilityValue, §3.4.1.5 ChunkIntTaskParameterDefinition,
§3.3.1 AmountRequirement. validation.md's per-field table now cites the
owning section for every field.
Tests: flatten and skip at both validation and job creation, mixed
literal + expansion ranges, per-element limit and charset violations,
all-null lists rejected after resolution, and the LIST[PATH]-in-range
test flips to asserting the flatten. Full conformance suite passes
(1,133/1,133).
BREAKING CHANGE: in task-parameter range elements and attribute
values, a whole-field expression resolving to `null` now skips the
element and a list result flattens inline (previously an error on this
branch, and the list's display form as a single element before that).
A range or attribute list emptied entirely by null-skips fails job
creation.
Signed-off-by: Mark <399551+mwiebe@users.noreply.github.com>
Review feedback: validation.md justified rejecting whole-field nulls by
asserting a minimum length of 1 on the string fields, but nothing
enforced that minimum on resolved values — a whole-field expression
resolving to the empty string passed every stage. The claim was also
wrong for one field: §4.4.2 sets the environment variable value minimum
at 0 characters, so empty env values are legal and stay unchecked.
Enforce the minima the spec actually sets, at both stages:
- job name (§1.1.1 minimum 1): rejected statically when fully static
("must not resolve to an empty string.") and re-checked on the
resolved value at job creation. The raw-text pass only sees literal
names.
- STRING/PATH range elements (§3.4.2 minimum 1): the per-element check
covers both a static empty string and an empty element of a flattened
list at validation; job creation's existing empty check, which
applied only to PATH elements, now covers STRING elements too (the
spec's constraint is on <TaskParameterStringValue>, which both share).
- attribute values already reject empties via
validate_attribute_capability_value.
Also fixes the job-name maximum-length message to report characters
(it compared characters but still printed the byte count), and
tightens the empty-PATH-element test to an exact-message assertion.
validation.md now states which fields have a resolved minimum and that
env values deliberately have none.
Signed-off-by: Mark <399551+mwiebe@users.noreply.github.com>
mwiebe
force-pushed
the
feat/group-a-resolved-value-validation
branch
from
September 11, 2026 22:38
e4a0bce to
46cb1f6
Compare
Review feedback caught two stragglers from the bytes-to-characters conversion: 1. The range-element length check compares characters but the count interpolated into its message was still s.len() — bytes — so a 1,200-character non-ASCII element reported "(2400 chars)". The job-name message had the same split and was already fixed; this one was missed. Hoist the count into a local used by both the comparison and the message. 2. The command length check — the one remaining limit in structure.rs not confined to ASCII by a charset pattern — still compared bytes, so a 600-character non-ASCII command (1,200+ bytes) was falsely rejected against the 1,024-character limit. Convert both, and for a single unit everywhere also convert the checks that are ASCII-confined in practice (amount and attribute capability names, the combination expression, environment variable names): bytes equal characters for values their patterns accept, but the length diagnostics fire before the pattern checks, so counting characters keeps them truthful for invalid non-ASCII input too. Every remaining .len() comparison in the validation pass is an element count. Tests pin the range-element message count (1,200 two-byte characters routed through a parameter so job creation's diagnostic is exercised) and the command limit at both sides of the boundary in two-byte characters. Signed-off-by: Mark <399551+mwiebe@users.noreply.github.com>
Review feedback: the int? target made the timeout resolver's catch-all
arm reachable for non-positive integers — {{ 0 }}, {{ '0' }}, and
{{ 0 - 5 }} all coerce to Int before the positivity guard, fell through
Int(n) if n > 0, and produced 'timeout must be a positive integer, got
Int(0)' — a Rust Debug rendering of an internal enum in a user-facing
error.
Mirror resolve_notify_period_seconds: accept Int(n) unconditionally and
reject non-positive values with a dedicated check that interpolates the
value ('got '-5''), leaving the catch-all for genuinely unexpected
types, which the int? target makes unreachable in practice.
Unit tests pin the exact message for all three coercion routes and the
positive/null paths.
Signed-off-by: Mark <399551+mwiebe@users.noreply.github.com>
Review feedback: environment variable values were the one constrained field with no enforcement stage after template validation — validation only catches statically knowable violations (an interpolation of an unresolved symbol contributes 0 to its lower bound), so a value that only becomes long at session time entered the process environment unchecked, while every sibling field re-checks at job creation or in the runtime. Enforce Template Schemas §4.4.2's 2048-character maximum where the value is resolved, alongside the existing NUL-byte guard, via a session-side constant kept in sync with the model's EffectiveLimits::max_env_var_value_len (which is not exported). Integration tests pin the exact message one character over the limit and acceptance exactly at it; specs/sessions/session.md documents the check in the enter_environment steps. Signed-off-by: Mark <399551+mwiebe@users.noreply.github.com>
Review feedback plus follow-up: list flattening and null-skipping make the resolved element count of anyOf/allOf unrelated to the count decode validated, in both directions. Flattening grows lists past decode-time rules: a single-valued attribute (attr.worker.os.family / attr.worker.cpu.arch) could carry an allOf with more than one element via a flattening expression, and a flattening expression over a longer list could exceed the 50-element cap. Re-apply both rules at job creation alongside the existing emptiness re-check, with the same "after resolution" wording — the sibling range path already re-checks its element cap after resolution for exactly this reason. Null-skips shrink lists below decode-time counts: a single-valued allOf built from two complementary conditionals always resolves to exactly one element, but decode counted template elements unconditionally and falsely rejected the pattern. Gate the decode-time "> 1" check on all elements being literal — the same gating the attribute value checks in that function already use — deferring expression-bearing lists to the job-creation re-check. The generic 50-element cap is deliberately not relaxed: it applies to the template element count regardless of expressions, so a deferred single-valued list is still bounded at decode. Tests: the flatten violations (a two-element LIST parameter flattened into a single-valued allOf; 51 flattened elements) and their passing controls (exactly 50; single-valued anyOf flattening to several candidates); conditional range elements skipping down (three template elements resolving to two, either branch); the complementary- conditional allOf resolving to one element for both parameter values and rejected when both conditionals survive; and the 50-element decode backstop on a deferred single-valued list. Signed-off-by: Mark <399551+mwiebe@users.noreply.github.com>
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.
What was the problem/requirement? (What/Why)
The 2023-09 Template Schemas place constraints on what several format-string fields resolve to — the spec's own phrasing is "after the format string has been resolved." A job name must resolve to ≤ 128 characters (512 with FEATURE_BUNDLE_1); an attribute capability value to ≤ 100 identifier-like characters; a task-parameter range element to ≤ 1024; an environment variable value to ≤ 2048;
notifyPeriodInSecondsto a positive integer ≤ 600. Call these the spec-mandated resolved-value constraints — the fields whose limits the spec applies to the value an interpolation produces, in contrast to fields the spec deliberately leaves uncapped (command,args, embedded-filedata).Before this change, none of them were enforced at template validation time for interpolated values.
openjd checkevaluates every format-string expression during validation (that is how static type checking works: unknown symbols areunresolvedplaceholders and evaluation propagates them) — but it threw the results away. Concretely:name: "{{ 'A' * 600 }}"passedcheckand failed at job submission.name: "{{ Param.X }}{{ 'A' * 600 }}"— guaranteed over-limit whateverParam.Xis — passedcheckand job submission logic that only sees the resolved whole.notifyPeriodInSeconds: "{{ 300 + 400 }}"(statically 700 > 600) passedcheckand failed on the worker, mid-job.chunksformat strings were never validated in the format-string pass at all: a typo'd expression surfaced only at job creation.Investigating the fix surfaced a second, related problem: resolution itself deviated from the spec's target-type rules. The Expression Language spec (§1.3.2 "Evaluation Within Template Schemas") derives a target type for every field's whole-field expression from its schema context —
Tfor required fields,T?for optional ones (null= field omitted) — and the Template Schemas doc explicitly mandatesint?fortimeout/notifyPeriodInSecondsandstring?for a deferred cancelationmode. Our gates resolved all of these untargeted (render to display text, trim, parse). Observable symptoms:timeout: "{{ 120.0 }}"failed at run time with "must be a positive integer, got Float" where the spec's coercion accepts 120; a whole-fieldnullin a required string field silently became the empty string (which the same fields' minimum-length-1 constraints forbid); an amount bound resolving tonullwas a parse error instead of "field omitted".The requirement: enforce these resolved-value constraints at the earliest stage where a violation is knowable, and make validation and resolution agree exactly on what a field accepts — full staging rationale in the design document.
What was the solution? (How)
Three commits, ordered so each layer builds on the previous. The key invariant throughout: a field's validation-time target type must equal its resolution-time target type, or validation would reject values resolution accepts (or vice versa).
fix(sessions)!— spec-mandated targets in the session runtime.resolve_action_timeout,resolve_notify_period_seconds, andresolve_effective_cancelationnow passint?/int?/string?viawith_target_type, so whole-field expressions coerce per the spec ({{ 120.0 }}→ 120,{{ '600' }}→ 600,null→ field unset). Multi-segment strings concatenate as before and now trim before parsing, matching the reference implementation'sint()leniency.feat(model)!— schema-derived targets at job creation. Job name, STRING/PATH range elements, and attribute capability values resolve with targetstring; amountmin/maxwithfloat?(a whole-fieldnullnow means the bound is unset); chunksdefaultTaskCountwithintand the optionaltargetRuntimeSecondswithint?(so{{ 4.0 }}coerces to 4); environment variable values withstringin the session runtime. Since there is nolist → stringornull → stringcoercion, a whole-field list ornullin a required string field is now an error instead of silently rendering its display form.feat(model)!— gate 1: the validation layer.validate_fsgains a per-fieldResolvedConstraintapplied to theStaticResolutionthat pass 8 already computes (the feat(expr)!: return StaticResolution from FormatString::validate_expressions #373 mechanism), in two stages:min_resolved_string_lenis a guaranteed lower bound on every possible run-time resolution (unresolved segments contribute 0), so a bound past the field's limit is a certain violation and failscheckwithout knowing the unresolved parts. This is what catches"{{ Param.X }}{{ 'A' * 600 }}".checktime with the same error messages (notifyPeriodInSeconds must not exceed 600.,value 'linuxx' is not valid for attr.worker.os.family., …).Numeric fields get a soft 100-character bound rather than an exact one — leading zeros are legal and whitespace is trimmed, so no exact maximum exists, but an i64 needs at most 20 characters and nothing reasonable exceeds 100. Standard attribute capabilities get a sharper bound: no resolution longer than the longest allowed value (e.g. 7 for
attr.worker.os.family) can ever conform. Literal (non-interpolated) fields are excluded — the existing raw-text passes already check those, and for literals raw text and resolved value coincide.What is the impact of this change?
ResolvedConstraintis private to the validation pass, and the sessions functions arepub(crate).openjd-expris untouched (this consumes the API shipped in feat(expr)!: return StaticResolution from FormatString::validate_expressions #373).openjd checkinstead of failing later — or, for environment variable values and chunks fields, instead of never failing.How was this change tested?
cargo test --workspace: all suites pass, including 2,032 tests inopenjd-modeland the sessions suites. (Two pre-existing Windows cross-user logon test failures on the development machine are environment-specific and unrelated.)tests/integration/test_resolved_value_constraints.rscover, per field: a fully static over-limit value failscheckwith the exact path and message; a partially-unresolved value whose lower bound alone exceeds the limit fails; and under-limit / fully-unresolved controls pass. Includes the target-type behaviors:{{ 120.0 }}timeout and{{ 4.0 }}defaultTaskCount valid; whole-fieldnull/list in string fields rejected with the resolution diagnostic;nullinside surrounding text still interpolating as empty;{{ null }}amounts andtargetRuntimeSecondstreated as unset.LIST[PATH]-parameter-as-range-element test now asserts rejection, with a comment explaining the alternative).cargo clippy --all-features --all-targets --workspace -- -D warningsis clean;cargo fmtclean;cargo docbuilds without warnings.Was this change documented?
specs/model/validation.mdgains a "Spec-Mandated Resolved-Value Constraints" section: the two-stage check, the per-field table (target type, bound, fully-static check), the §1.3.2 general rule, the named consequences ofstringtargets, and the literals exclusion.specs/model/job-creation.mddocuments the chunks/amount targeting;specs/sessions/runners.mddocuments the action-field targeting.ResolvedConstraintand the resolution helpers explain the invariant and cite the spec sections.specs/resolved-value-limits.mdondocs/resolved-value-limits-designshould mark gate 1 implemented and record the remaining open question.Is this a breaking change?
Yes — all three commits carry
BREAKING CHANGEfooters. Summary of observable changes:openjd check(previously accepted, failing later or never).nullor list-valued expressions in required string fields (job name, range elements, attribute values, environment variable values) are now resolution errors; there is nonull → stringorlist → stringcoercion. A bareLIST[*]parameter reference as a single range element previously rendered the list's display form as one element and is now rejected.timeout/notifyPeriodInSeconds/mode/chunks fields coerce whole-field expressions per their targets: whole-number floats and numeric strings are accepted where they previously errored; uncoercible values fail with coercion diagnostics instead of hand-written parse messages.min/maxresolving tonullnow means the bound is unset (previously a parse error); non-numeric or non-finite amount strings fail with coercion diagnostics.These changes have not reached production consumers; landing them now, before the behavior is depended upon, is the point of doing this in one coordinated change.
Does this change impact security?
No
Follow-ups
string? | list[string]with skip/flatten semantics (asargsdoes). They currently target plainstring(no skip/flatten). Needs reference-implementation cross-checking and an upstream clarification issue; the chunksintreading adopted here is worth confirming upstream at the same time.openjd-model-for-pythonbehavior and file an upstream clarification (pre-existing action item from the design document).By submitting this pull request, I confirm that you can use, modify, copy, and redistribute this contribution, under the terms of your choice.