diff --git a/conformance-tests/2023-09/FEATURE_BUNDLE_1/jobs/3.3.1--amount-max-format-string-resolves-to-non-number.invalid.test.yaml b/conformance-tests/2023-09/FEATURE_BUNDLE_1/jobs/3.3.1--amount-max-format-string-resolves-to-non-number.invalid.test.yaml new file mode 100644 index 00000000..428a54f2 --- /dev/null +++ b/conformance-tests/2023-09/FEATURE_BUNDLE_1/jobs/3.3.1--amount-max-format-string-resolves-to-non-number.invalid.test.yaml @@ -0,0 +1,36 @@ +# hostRequirements.amounts[].max may be a format string under FEATURE_BUNDLE_1 (§3.3.1 L960), +# the same as `min` at §3.3.1 L958. The numeric constraint cannot be checked at decode, so it +# must be checked after resolution at job creation. Here the parameter resolves to a non-numeric +# string and the run must fail. +# +# Companion to 3.3.1--amount-min-format-string-resolves-to-non-number.invalid.test.yaml. `min` +# and `max` carry different types in the spec, against , so +# they are covered separately rather than assumed symmetric. +# +# Spec references, Template Schemas 2023-09: +# §3.3.1 L960 amounts[].max may be `` under FEATURE_BUNDLE_1, @fmtstring +# https://github.com/OpenJobDescription/openjd-specifications/blob/mainline/wiki/2023-09-Template-Schemas.md?plain=1#L960 +# §3.3.1 L958 the min counterpart, covered separately because the types differ +# https://github.com/OpenJobDescription/openjd-specifications/blob/mainline/wiki/2023-09-Template-Schemas.md?plain=1#L958 +template: + specificationVersion: jobtemplate-2023-09 + extensions: + - FEATURE_BUNDLE_1 + name: TestJob + parameterDefinitions: + - name: CpuMax + type: STRING + default: "abc" + steps: + - name: Step1 + hostRequirements: + amounts: + - name: amount.worker.vcpu + max: "{{Param.CpuMax}}" + script: + actions: + onRun: + command: python + args: + - -c + - print(r'RAN') diff --git a/conformance-tests/2023-09/FEATURE_BUNDLE_1/jobs/3.3.1--amount-min-format-string-resolves-to-non-number.invalid.test.yaml b/conformance-tests/2023-09/FEATURE_BUNDLE_1/jobs/3.3.1--amount-min-format-string-resolves-to-non-number.invalid.test.yaml new file mode 100644 index 00000000..f75a7062 --- /dev/null +++ b/conformance-tests/2023-09/FEATURE_BUNDLE_1/jobs/3.3.1--amount-min-format-string-resolves-to-non-number.invalid.test.yaml @@ -0,0 +1,35 @@ +# hostRequirements.amounts[].min may be a format string under FEATURE_BUNDLE_1 (§3.3.1 L958). +# The numeric constraint cannot be checked at decode, so it must be checked after resolution at +# job creation. Here the parameter resolves to a non-numeric string and the run must fail. +# +# This is the negative half of the amount-resolution gap. The positive half, asserting that min +# and max resolve to the CORRECT numbers, is not writable today: no openjd CLI surfaces resolved +# host requirements, in summary --output json or anywhere else. +# +# Spec references, Template Schemas 2023-09: +# §3.3.1 L958 amounts[].min may be `` under FEATURE_BUNDLE_1, @fmtstring +# https://github.com/OpenJobDescription/openjd-specifications/blob/mainline/wiki/2023-09-Template-Schemas.md?plain=1#L958 +# §7.4 L2017 format strings not annotated @fmtstring[host] resolve at job creation +# https://github.com/OpenJobDescription/openjd-specifications/blob/mainline/wiki/2023-09-Template-Schemas.md?plain=1#L2017 +template: + specificationVersion: jobtemplate-2023-09 + extensions: + - FEATURE_BUNDLE_1 + name: TestJob + parameterDefinitions: + - name: CpuMin + type: STRING + default: "abc" + steps: + - name: Step1 + hostRequirements: + amounts: + - name: amount.worker.vcpu + min: "{{Param.CpuMin}}" + script: + actions: + onRun: + command: python + args: + - -c + - print(r'RAN') diff --git a/conformance-tests/2023-09/FEATURE_BUNDLE_1/jobs/proposed/3.3.1--amount-max-format-string-resolves-to-zero.invalid.test.yaml b/conformance-tests/2023-09/FEATURE_BUNDLE_1/jobs/proposed/3.3.1--amount-max-format-string-resolves-to-zero.invalid.test.yaml new file mode 100644 index 00000000..35fa2fb8 --- /dev/null +++ b/conformance-tests/2023-09/FEATURE_BUNDLE_1/jobs/proposed/3.3.1--amount-max-format-string-resolves-to-zero.invalid.test.yaml @@ -0,0 +1,43 @@ +# hostRequirements.amounts[].max is typed (§3.3.1 L959-960), and §3.3.1 confirms +# it separately from min, which is . So 0 is legal for min and illegal for +# max. +# +# Under FEATURE_BUNDLE_1 max may be a format string, and the positivity constraint cannot be +# checked at decode because the value is not yet known. It must be re-checked after resolution +# at job creation. Here the parameter resolves to 0. +# +# This is the only assertion in the suite that distinguishes max semantics from min semantics +# for a resolved value. A literal max: 0 is already rejected at decode by +# base/job_templates/3.3.1--amount-max-zero.invalid.yaml, so only the resolved path is +# unguarded. +# +# Spec references, Template Schemas 2023-09: +# §3.3.1 L959 amounts[].max is ``, so 0 is illegal +# https://github.com/OpenJobDescription/openjd-specifications/blob/mainline/wiki/2023-09-Template-Schemas.md?plain=1#L959 +# §3.3.1 L957 amounts[].min is ``, so 0 IS legal there +# https://github.com/OpenJobDescription/openjd-specifications/blob/mainline/wiki/2023-09-Template-Schemas.md?plain=1#L957 +# §3.3.1 L960 max may be a format string under FEATURE_BUNDLE_1, so the bound defers past decode +# https://github.com/OpenJobDescription/openjd-specifications/blob/mainline/wiki/2023-09-Template-Schemas.md?plain=1#L960 +template: + specificationVersion: jobtemplate-2023-09 + extensions: + - FEATURE_BUNDLE_1 + name: TestJob + parameterDefinitions: + - name: CpuMax + type: INT + # form, so the positivity check must survive both coercions. + default: '0' + steps: + - name: Step1 + hostRequirements: + amounts: + - name: amount.worker.vcpu + max: "{{Param.CpuMax}}" + script: + actions: + onRun: + command: python + args: + - -c + - print(r'RAN') diff --git a/conformance-tests/2023-09/FEATURE_BUNDLE_1/jobs/proposed/README.md b/conformance-tests/2023-09/FEATURE_BUNDLE_1/jobs/proposed/README.md new file mode 100644 index 00000000..faf99bab --- /dev/null +++ b/conformance-tests/2023-09/FEATURE_BUNDLE_1/jobs/proposed/README.md @@ -0,0 +1,46 @@ +# Proposed FEATURE_BUNDLE_1 job fixtures: post-resolution bound checks + +These fixtures are believed spec-correct and currently fail at least one reference +implementation. They live here rather than in `../` because the runner discovers test +files with a non-recursive glob, so `proposed/` is excluded and the suite stays green. +Promotion is `git mv` up one directory with no edit to the fixture. + +| Fixture | Construct | Observed | +|---|---|---| +| `3.3.1--amount-max-format-string-resolves-to-zero.invalid.test.yaml` | `amounts[].max: "{{Param.CpuMax}}"` where the parameter default is `0`, against the `` type at §3.3.1 L959 | openjd-model Python rejects at job creation. The Rust CLI accepts the resolved `0` and runs the job to completion | + +## Classification + +Implementation fix. + +`max` is `` while `min` is ``, so `0` is legal for one and +not the other. A literal `max: 0` is already rejected at decode by +`../../job_templates/3.3.1--amount-max-zero.invalid.yaml`. Under FEATURE_BUNDLE_1 the value +may be a format string, in which case the positivity constraint cannot be checked at decode +and must be re-checked after resolution. The Python path does this; the Rust path defers the +check and never resumes it. + +This is the same defect shape as the two fixtures in +`../../../TASK_CHUNKING/jobs/proposed/` and `../../../base/jobs/proposed/`: validation +correctly deferred for a format string, never resumed. Reviewers may prefer to treat all of +them as one issue. + +The green twin, +`../3.3.1--amount-max-format-string-resolves-to-non-number.invalid.test.yaml`, covers a +`max` that resolves to a non-numeric string. Both implementations reject that, which is why +it sits in the live suite. The divergence recorded here is specific to the positivity bound +rather than to numeric parsing. + +Not covered by any fixture, here or in `../`: the positive case asserting a resolved `max` +is the correct number. No `openjd` CLI surfaces resolved host requirements, and the runner +asserts only on stdout and task status, so a host requirement has no observable effect on a +single-host run. The negative cases are the reachable half. + +## Spec references, Template Schemas 2023-09 + +- [§3.3.1 L959](https://github.com/OpenJobDescription/openjd-specifications/blob/mainline/wiki/2023-09-Template-Schemas.md?plain=1#L959) amounts[].max is ``, so 0 is illegal +- [§3.3.1 L957](https://github.com/OpenJobDescription/openjd-specifications/blob/mainline/wiki/2023-09-Template-Schemas.md?plain=1#L957) amounts[].min is ``, so 0 IS legal there +- [§3.3.1 L960](https://github.com/OpenJobDescription/openjd-specifications/blob/mainline/wiki/2023-09-Template-Schemas.md?plain=1#L960) max may be a format string under FEATURE_BUNDLE_1, so the bound defers past decode + +Line numbers are a locator for where each claim was verified. The section numbers are the +durable reference if the spec is re-flowed. diff --git a/conformance-tests/2023-09/TASK_CHUNKING/jobs/default-task-count-format-string.test.yaml b/conformance-tests/2023-09/TASK_CHUNKING/jobs/default-task-count-format-string.test.yaml new file mode 100644 index 00000000..c6f85274 --- /dev/null +++ b/conformance-tests/2023-09/TASK_CHUNKING/jobs/default-task-count-format-string.test.yaml @@ -0,0 +1,58 @@ +# chunks.defaultTaskCount is annotated @fmtstring (§3.4.1.5 L1263), so it may be a parameter +# reference and must be resolved at job creation. This case pins the resolved value by its +# observable effect: ChunkSize=3 over the range 1-8 can only produce the boundaries 1-3, 4-6, +# 7-8. An implementation that reads the pre-resolution template value fails here rather than at +# some later stage. +# +# The parameter default is written in the string form deliberately. A native +# `default: 3` would exercise only the format-string layer; the string form chains both, and the +# assertion is identical, so it is strictly stronger at no cost. +# +# Status-only by design: a 3-chunk run cannot self-assert, because each task sees only its own +# output (see the conformance-tests README). +# +# Spec references, Template Schemas 2023-09: +# §3.4.1.5 L1263 chunks.defaultTaskCount is ` | ` and @fmtstring +# https://github.com/OpenJobDescription/openjd-specifications/blob/mainline/wiki/2023-09-Template-Schemas.md?plain=1#L1263 +# §2.3 L306 INT parameter default is ` | ` +# https://github.com/OpenJobDescription/openjd-specifications/blob/mainline/wiki/2023-09-Template-Schemas.md?plain=1#L306 +# §7.4 L2017 format strings not annotated @fmtstring[host] resolve at job creation +# https://github.com/OpenJobDescription/openjd-specifications/blob/mainline/wiki/2023-09-Template-Schemas.md?plain=1#L2017 +template: + specificationVersion: jobtemplate-2023-09 + extensions: + - TASK_CHUNKING + name: ChunkTest + parameterDefinitions: + - name: ChunkSize + type: INT + # form, with a leading zero. INT parameter defaults are + # ` | ` in base 2023-09 (§2.3 L306), so this composes the two + # coercions the fixture cares about: the string default must parse to 3, and the + # format string below must then resolve to it. The boundaries assert both at once. + default: '03' + steps: + - name: Step1 + parameterSpace: + taskParameterDefinitions: + - name: Frame + type: CHUNK[INT] + range: 1-8 + chunks: + defaultTaskCount: "{{Param.ChunkSize}}" + rangeConstraint: CONTIGUOUS + script: + actions: + onRun: + command: python + args: + - -c + - print(r'CHUNK:{{Task.Param.Frame}}') +expected: + output: + - 'CHUNK:1-3' + - 'CHUNK:4-6' + - 'CHUNK:7-8' + forbidden: + - 'CHUNK:1-8' + - 'CHUNK:9' diff --git a/conformance-tests/2023-09/TASK_CHUNKING/jobs/proposed/README.md b/conformance-tests/2023-09/TASK_CHUNKING/jobs/proposed/README.md new file mode 100644 index 00000000..fb9709cc --- /dev/null +++ b/conformance-tests/2023-09/TASK_CHUNKING/jobs/proposed/README.md @@ -0,0 +1,39 @@ +# Proposed TASK_CHUNKING job fixtures: post-resolution bound checks + +These fixtures are believed spec-correct and currently fail at least one reference +implementation. They live here rather than in `../` because the runner discovers test +files with a non-recursive glob, so `proposed/` is excluded and the suite stays green. +Promotion is `git mv` up one directory with no edit to the fixture. + +| Fixture | Construct | Observed | +|---|---|---| +| `default-task-count-format-string-resolves-to-zero.invalid.test.yaml` | `chunks.defaultTaskCount: "{{Param.ChunkSize}}"` where the parameter default is `0`, against the documented minimum of 1 (§3.4.1.5 L1276) | openjd-model Python rejects at job creation. The Rust CLI reports `Template ... passes validation checks` and then runs, logging `Frame(CHUNK[INT]) = 1-1`, so the resolved `0` is treated as `1` | + +## Classification + +Implementation fix. + +The minimum-of-1 bound cannot be checked at decode when the value is a format string, +because the value is not yet known, and `TASK_CHUNKING/job_templates/default-task-count-zero.invalid.yaml` +already covers the literal `0` case at decode. The bound must therefore be re-checked +after resolution at job creation. The Python path does this; the Rust path defers the +check and never resumes it. + +Cross-check: in `openjd-model/src/template/validate_v2023_09/task_chunking.rs` the +`>= 1` comparison is guarded on the chunk count being a literal integer, which is +correct for decode-time validation. No equivalent check appears on the job-creation +path, so a format-string value reaches the scheduler unvalidated. + +A green twin ships in `../default-task-count-format-string.test.yaml`, which asserts +that a format-string `defaultTaskCount` resolves and produces the expected chunk +boundaries. That case passes both implementations, so the divergence recorded here is +specific to the bound check rather than to resolution itself. + +## Spec references, Template Schemas 2023-09 + +- [§3.4.1.5 L1263](https://github.com/OpenJobDescription/openjd-specifications/blob/mainline/wiki/2023-09-Template-Schemas.md?plain=1#L1263) chunks.defaultTaskCount is @fmtstring, so the bound defers past decode +- [§3.4.1.5 L1276](https://github.com/OpenJobDescription/openjd-specifications/blob/mainline/wiki/2023-09-Template-Schemas.md?plain=1#L1276) defaultTaskCount minimum value: 1, the bound not re-checked after resolution +- [§7.4 L2017](https://github.com/OpenJobDescription/openjd-specifications/blob/mainline/wiki/2023-09-Template-Schemas.md?plain=1#L2017) format strings not annotated @fmtstring[host] resolve at job creation + +Line numbers are a locator for where each claim was verified. The section numbers are the +durable reference if the spec is re-flowed. diff --git a/conformance-tests/2023-09/TASK_CHUNKING/jobs/proposed/default-task-count-format-string-resolves-to-zero.invalid.test.yaml b/conformance-tests/2023-09/TASK_CHUNKING/jobs/proposed/default-task-count-format-string-resolves-to-zero.invalid.test.yaml new file mode 100644 index 00000000..6f1eeb3a --- /dev/null +++ b/conformance-tests/2023-09/TASK_CHUNKING/jobs/proposed/default-task-count-format-string-resolves-to-zero.invalid.test.yaml @@ -0,0 +1,42 @@ +# chunks.defaultTaskCount has a minimum of 1. When the value is written as a format string the +# bound cannot be checked at decode, because the value is not yet known, so it must be checked +# after resolution at job creation. This case supplies a parameter that resolves to 0 and must +# therefore fail. +# +# .invalid.test.yaml rather than .invalid.yaml: the template is statically valid and the error +# only fires once the expression is evaluated. +# +# Spec references, Template Schemas 2023-09: +# §3.4.1.5 L1263 chunks.defaultTaskCount is @fmtstring, so the bound cannot be checked at decode +# https://github.com/OpenJobDescription/openjd-specifications/blob/mainline/wiki/2023-09-Template-Schemas.md?plain=1#L1263 +# §3.4.1.5 L1276 defaultTaskCount minimum value: 1 +# https://github.com/OpenJobDescription/openjd-specifications/blob/mainline/wiki/2023-09-Template-Schemas.md?plain=1#L1276 +# §2.3 L306 INT parameter default is ` | ` +# https://github.com/OpenJobDescription/openjd-specifications/blob/mainline/wiki/2023-09-Template-Schemas.md?plain=1#L306 +template: + specificationVersion: jobtemplate-2023-09 + extensions: + - TASK_CHUNKING + name: ChunkTest + parameterDefinitions: + - name: ChunkSize + type: INT + # form, so the bound check must survive both coercions. + default: '0' + steps: + - name: Step1 + parameterSpace: + taskParameterDefinitions: + - name: Frame + type: CHUNK[INT] + range: 1-8 + chunks: + defaultTaskCount: "{{Param.ChunkSize}}" + rangeConstraint: CONTIGUOUS + script: + actions: + onRun: + command: python + args: + - -c + - print(r'CHUNK:{{Task.Param.Frame}}') diff --git a/conformance-tests/2023-09/TASK_CHUNKING/jobs/target-runtime-seconds-format-string.test.yaml b/conformance-tests/2023-09/TASK_CHUNKING/jobs/target-runtime-seconds-format-string.test.yaml new file mode 100644 index 00000000..a9a86fa3 --- /dev/null +++ b/conformance-tests/2023-09/TASK_CHUNKING/jobs/target-runtime-seconds-format-string.test.yaml @@ -0,0 +1,56 @@ +# chunks.targetRuntimeSeconds is annotated @fmtstring (§3.4.1.5 L1264), so it may be a parameter +# reference and must be resolved at job creation. +# +# The resolved value is 0, and §3.4.1.5 says that when targetRuntimeSeconds is 0 a scheduler +# should ignore it and use defaultTaskCount for every chunk. That makes the assertion +# deterministic: the boundaries must be exactly those defaultTaskCount implies, with no adaptive +# resizing. A non-zero resolved value would let a conforming scheduler adjust the chunk size, so +# this case pins the one resolved value whose effect is fixed. +# +# An implementation that reads the pre-resolution template value sees the literal +# "{{Param.TargetRuntime}}" instead of 0 here. +# +# Spec references, Template Schemas 2023-09: +# §3.4.1.5 L1264 chunks.targetRuntimeSeconds is @fmtstring +# https://github.com/OpenJobDescription/openjd-specifications/blob/mainline/wiki/2023-09-Template-Schemas.md?plain=1#L1264 +# §3.4.1.5 L1280 when the value is 0 a scheduler should ignore it and use defaultTaskCount +# https://github.com/OpenJobDescription/openjd-specifications/blob/mainline/wiki/2023-09-Template-Schemas.md?plain=1#L1280 +# §2.3 L306 INT parameter default is ` | ` +# https://github.com/OpenJobDescription/openjd-specifications/blob/mainline/wiki/2023-09-Template-Schemas.md?plain=1#L306 +template: + specificationVersion: jobtemplate-2023-09 + extensions: + - TASK_CHUNKING + name: ChunkTest + parameterDefinitions: + - name: TargetRuntime + type: INT + # form: the string must parse to 0 before the format string can resolve to + # it, so this fixture covers both coercion layers rather than only the second. + default: '00' + steps: + - name: Step1 + parameterSpace: + taskParameterDefinitions: + - name: Frame + type: CHUNK[INT] + range: 1-6 + chunks: + defaultTaskCount: 2 + targetRuntimeSeconds: "{{Param.TargetRuntime}}" + rangeConstraint: CONTIGUOUS + script: + actions: + onRun: + command: python + args: + - -c + - print(r'CHUNK:{{Task.Param.Frame}}') +expected: + output: + - 'CHUNK:1-2' + - 'CHUNK:3-4' + - 'CHUNK:5-6' + forbidden: + - 'CHUNK:1-6' + - 'CHUNK:7' diff --git a/conformance-tests/2023-09/WRAP_ACTIONS/jobs/wrap-cancelation-notify-period-fmtstring-resolved.test.yaml b/conformance-tests/2023-09/WRAP_ACTIONS/jobs/wrap-cancelation-notify-period-fmtstring-resolved.test.yaml new file mode 100644 index 00000000..1a559db0 --- /dev/null +++ b/conformance-tests/2023-09/WRAP_ACTIONS/jobs/wrap-cancelation-notify-period-fmtstring-resolved.test.yaml @@ -0,0 +1,79 @@ +# cancelation.notifyPeriodInSeconds is annotated @fmtstring under FEATURE_BUNDLE_1 (§5.3.2 +# L1835), so it may be a parameter reference. Neither `openjd run` nor `openjd summary` surfaces +# the field directly, so the resolved value would normally be unobservable and only assertable +# at level 2. +# +# WrappedAction.Cancelation.NotifyPeriodInSeconds (RFC 0008) closes that: the reflection +# variable carries the EFFECTIVE value, after schema defaults and after format-string +# resolution, so a wrap script can read back what the job actually got. +# +# The parameter's own default is 120 and the submitted value is 47. Both are forbidden as +# outputs alongside the unresolved literal, so the fixture fails if the implementation resolves +# nothing, resolves to the default, or reflects a stale value. +# +# Companion to WRAP_ACTIONS/jobs/wrap-cancelation-notify-period-injected.test.yaml, which covers +# the same reflection path for a literal notifyPeriodInSeconds. +# +# Spec references, Template Schemas 2023-09: +# §5.3.2 L1835 notifyPeriodInSeconds may be `` under FEATURE_BUNDLE_1, @fmtstring +# https://github.com/OpenJobDescription/openjd-specifications/blob/mainline/wiki/2023-09-Template-Schemas.md?plain=1#L1835 +# §4.3.1 L1630 WrappedAction.Cancelation.NotifyPeriodInSeconds reflection variable +# https://github.com/OpenJobDescription/openjd-specifications/blob/mainline/wiki/2023-09-Template-Schemas.md?plain=1#L1630 +# §2.3 L306 INT parameter value is ` | ` +# https://github.com/OpenJobDescription/openjd-specifications/blob/mainline/wiki/2023-09-Template-Schemas.md?plain=1#L306 +# RFC 0008 wrap hook template variables +# https://github.com/OpenJobDescription/openjd-specifications/blob/mainline/rfcs/0008-environment-wrap-actions.md +template: + specificationVersion: jobtemplate-2023-09 + extensions: + - FEATURE_BUNDLE_1 + name: WrapNotifyPeriodFmtstringResolved + parameterDefinitions: + - name: NotifyPeriod + type: INT + minValue: 1 + maxValue: 600 + default: 120 + steps: + - name: Step1 + script: + actions: + onRun: + command: echo + args: ["placeholder"] + cancelation: + mode: NOTIFY_THEN_TERMINATE + notifyPeriodInSeconds: "{{Param.NotifyPeriod}}" +parameters: + # form on the SUBMITTED value, not just the definition default. Three + # coercions chain here: the string parses to 47, the format string on the action resolves + # to it, and the reflection variable reports the effective value. + NotifyPeriod: '047' +environments: +- specificationVersion: environment-2023-09 + extensions: + - WRAP_ACTIONS + - EXPR + environment: + name: WrapEnv + script: + actions: + onWrapEnvEnter: + command: bash + args: ["-c", "{{ repr_sh(WrappedAction.Command) }} {{ repr_sh(WrappedAction.Args) }}"] + onWrapTaskRun: + command: bash + # `<...>` sentinel wrap so an empty or null value renders unambiguously as `NP=<>`. + args: ["-c", "echo \"NP=<{{WrappedAction.Cancelation.NotifyPeriodInSeconds}}>\""] + onWrapEnvExit: + command: bash + args: ["-c", "{{ repr_sh(WrappedAction.Command) }} {{ repr_sh(WrappedAction.Args) }}"] +runOn: +- posix +expected: + output: + - "NP=<47>" + forbidden: + - "NP=<{{" + - "NP=<>" + - "NP=<120>" diff --git a/conformance-tests/2023-09/base/jobs/1.1--int-intstring-allowedvalues-violation.invalid.test.yaml b/conformance-tests/2023-09/base/jobs/1.1--int-intstring-allowedvalues-violation.invalid.test.yaml new file mode 100644 index 00000000..16be9707 --- /dev/null +++ b/conformance-tests/2023-09/base/jobs/1.1--int-intstring-allowedvalues-violation.invalid.test.yaml @@ -0,0 +1,32 @@ +# .allowedValues is `[ | , ... ]` in base +# 2023-09 (§2.3 L307). Membership must be decided numerically when the list holds the string +# form and the submitted value arrives as a number. +# +# The list is ['1', '2', '4'] as strings, and the submitted value is the number 3, which is +# absent. The job must be rejected at job creation. The paired positive case, submitting a +# member, is covered at template level by base/job_templates/2.3--allowed-values-intstring.yaml. +# +# '04' is used for the third element so an implementation comparing string forms rather than +# parsed integers would also fail to match a submitted 4, which the mutation check exercises. +# +# Spec references, Template Schemas 2023-09: +# §2.3 L307 allowedValues is `[ | , ... ]` +# https://github.com/OpenJobDescription/openjd-specifications/blob/mainline/wiki/2023-09-Template-Schemas.md?plain=1#L307 +template: + specificationVersion: jobtemplate-2023-09 + name: TestJob + parameterDefinitions: + - name: Count + type: INT + allowedValues: ['1', '2', '04'] + steps: + - name: Step1 + script: + actions: + onRun: + command: python + args: + - -c + - print(r'COUNT:{{Param.Count}}') +parameters: + Count: 3 diff --git a/conformance-tests/2023-09/base/jobs/1.1--int-intstring-bounds-satisfied.test.yaml b/conformance-tests/2023-09/base/jobs/1.1--int-intstring-bounds-satisfied.test.yaml new file mode 100644 index 00000000..2750f064 --- /dev/null +++ b/conformance-tests/2023-09/base/jobs/1.1--int-intstring-bounds-satisfied.test.yaml @@ -0,0 +1,40 @@ +# .minValue and .maxValue are ` | ` in base +# 2023-09 (§2.3 L308-309). When the bound is written as a string and the submitted value arrives +# as a number, the comparison must be numeric. A naive implementation that compares the string +# bound against the value lexically accepts and rejects the wrong things. +# +# Bounds here are the strings '10' and '100', and the submitted value is the number 50, which +# satisfies both. The paired negative is 1.1--int-intstring-bounds-violation.invalid.test.yaml. +# +# '010' is used for the lower bound so a lexical comparison against '50' would order them +# differently from the numeric comparison, making the two paths distinguishable. +# +# Spec references, Template Schemas 2023-09: +# §2.3 L308 minValue is ` | ` +# https://github.com/OpenJobDescription/openjd-specifications/blob/mainline/wiki/2023-09-Template-Schemas.md?plain=1#L308 +# §2.3 L309 maxValue is ` | ` +# https://github.com/OpenJobDescription/openjd-specifications/blob/mainline/wiki/2023-09-Template-Schemas.md?plain=1#L309 +template: + specificationVersion: jobtemplate-2023-09 + name: TestJob + parameterDefinitions: + - name: Count + type: INT + minValue: '010' + maxValue: '100' + steps: + - name: Step1 + script: + actions: + onRun: + command: python + args: + - -c + - print(r'COUNT:{{Param.Count}}') +parameters: + Count: 50 +expected: + output: + - 'COUNT:50' + forbidden: + - '{{Param.' diff --git a/conformance-tests/2023-09/base/jobs/1.1--int-intstring-bounds-violation.invalid.test.yaml b/conformance-tests/2023-09/base/jobs/1.1--int-intstring-bounds-violation.invalid.test.yaml new file mode 100644 index 00000000..309fdbfe --- /dev/null +++ b/conformance-tests/2023-09/base/jobs/1.1--int-intstring-bounds-violation.invalid.test.yaml @@ -0,0 +1,34 @@ +# Negative pair for 1.1--int-intstring-bounds-satisfied.test.yaml. +# +# minValue and maxValue are written as the strings '010' and '100', and the submitted value is +# the number 200. The bound must be interpreted numerically and the value rejected at job +# creation. An implementation that fails to parse a string bound, or that compares it lexically, +# admits this job. +# +# .invalid.test.yaml rather than .invalid.yaml: the template is statically valid and the error +# only fires once the submitted value is bound. +# +# Spec references, Template Schemas 2023-09: +# §2.3 L308 minValue is ` | ` +# https://github.com/OpenJobDescription/openjd-specifications/blob/mainline/wiki/2023-09-Template-Schemas.md?plain=1#L308 +# §2.3 L309 maxValue is ` | ` +# https://github.com/OpenJobDescription/openjd-specifications/blob/mainline/wiki/2023-09-Template-Schemas.md?plain=1#L309 +template: + specificationVersion: jobtemplate-2023-09 + name: TestJob + parameterDefinitions: + - name: Count + type: INT + minValue: '010' + maxValue: '100' + steps: + - name: Step1 + script: + actions: + onRun: + command: python + args: + - -c + - print(r'COUNT:{{Param.Count}}') +parameters: + Count: 200 diff --git a/conformance-tests/2023-09/base/jobs/1.1--path-default-joined-with-template-dir.test.yaml b/conformance-tests/2023-09/base/jobs/1.1--path-default-joined-with-template-dir.test.yaml new file mode 100644 index 00000000..193989c9 --- /dev/null +++ b/conformance-tests/2023-09/base/jobs/1.1--path-default-joined-with-template-dir.test.yaml @@ -0,0 +1,39 @@ +# §7.4 L2017 states that at job creation "PATH parameter defaults are joined with the job +# template directory". That join is a value transform, so the raw template and the resolved job +# differ, and a consumer reading the raw default gets a relative path. +# +# The existing fixture 1.1--validation-valid-path-default-within-template-dir.test.yaml cannot +# catch a missing join: its default is `./output` and it asserts the substring `/output`, which +# the un-joined `./output` also contains. An implementation that skipped the join entirely +# passes it. +# +# This fixture removes that escape. The default carries no leading `./`, so `RESULT:inputs/` +# appears only if the value was not joined, and it is forbidden. Only the path suffix is +# asserted, because the absolute prefix legitimately differs between implementations: one +# resolves symlinks and reports /private/var/..., the other reports /var/... +# +# Spec references, Template Schemas 2023-09: +# §7.4 L2017 PATH parameter defaults are joined with the job template directory at job creation +# https://github.com/OpenJobDescription/openjd-specifications/blob/mainline/wiki/2023-09-Template-Schemas.md?plain=1#L2017 +template: + specificationVersion: jobtemplate-2023-09 + name: TestJob + parameterDefinitions: + - name: InputDir + type: PATH + default: inputs/scene + steps: + - name: Step1 + script: + actions: + onRun: + command: python + args: ["-c", "print(r'RESULT:{{Param.InputDir}}')"] +runOn: +- posix +expected: + output: + - "/inputs/scene" + forbidden: + - "RESULT:inputs/" + - "RESULT:./" diff --git a/conformance-tests/2023-09/base/jobs/1.1--validation-valid-path-default-within-template-dir.test.yaml b/conformance-tests/2023-09/base/jobs/1.1--validation-valid-path-default-within-template-dir.test.yaml index 374dbd1d..47ca4338 100644 --- a/conformance-tests/2023-09/base/jobs/1.1--validation-valid-path-default-within-template-dir.test.yaml +++ b/conformance-tests/2023-09/base/jobs/1.1--validation-valid-path-default-within-template-dir.test.yaml @@ -1,3 +1,18 @@ +# Strengthened: the expected substrings alone could not distinguish a joined path from an +# un-joined one. The default is `./output`, and the POSIX assertion `/output` is a substring of +# the literal `./output`, so an implementation that skipped the template-directory join entirely +# still passed. Verified by running this fixture with the task hardcoded to print +# `OUTPUT:./output`, which passed. +# +# The forbidden entries close that. `OUTPUT:./` and `OUTPUT:output` can only appear if the value +# reaching the task was not joined with the job template directory. +# +# See also 1.1--path-default-joined-with-template-dir.test.yaml, which asserts the same +# behaviour with a default carrying no `./` prefix. +# +# Spec references, Template Schemas 2023-09: +# §7.4 L2017 PATH parameter defaults are joined with the job template directory at job creation +# https://github.com/OpenJobDescription/openjd-specifications/blob/mainline/wiki/2023-09-Template-Schemas.md?plain=1#L2017 template: specificationVersion: jobtemplate-2023-09 name: TestJob @@ -21,3 +36,6 @@ expected: - /output output_windows: - "\\output" + forbidden: + - 'OUTPUT:./' + - 'OUTPUT:output' diff --git a/conformance-tests/2023-09/base/jobs/1.1.1--resolved-job-name-value.test.yaml b/conformance-tests/2023-09/base/jobs/1.1.1--resolved-job-name-value.test.yaml new file mode 100644 index 00000000..6c58a295 --- /dev/null +++ b/conformance-tests/2023-09/base/jobs/1.1.1--resolved-job-name-value.test.yaml @@ -0,0 +1,45 @@ +# The job template `name` is annotated @fmtstring (§1.1 L38) and resolves at job creation. +# Existing base fixtures assert a PARALLEL substitution of the same parameter into a task +# argument, which proves the parameter resolved somewhere but says nothing about the name field. +# EXPR/jobs/7.3.1--job-name-step-name.test.yaml asserts the resolved name properly, but needs +# the EXPR extension to read {{ Job.Name }}. +# +# This asserts the name field itself at base level, through the implementation's own report of +# the job it ran. Both reference CLIs emit the resolved name twice, once as `Running job +# ''` and once as `Job: ` in the results block, so the assertion is written with +# anyOf and either satisfies it. +# +# The parameters block overrides both defaults. The default-derived name is forbidden, so a +# fixture that resolved against defaults instead of submitted values fails. +# +# Spec references, Template Schemas 2023-09: +# §1.1 L38 the job template name field is @fmtstring +# https://github.com/OpenJobDescription/openjd-specifications/blob/mainline/wiki/2023-09-Template-Schemas.md?plain=1#L38 +# §7.4 L2017 resolved at job creation, since it is not annotated @fmtstring[host] +# https://github.com/OpenJobDescription/openjd-specifications/blob/mainline/wiki/2023-09-Template-Schemas.md?plain=1#L2017 +template: + specificationVersion: jobtemplate-2023-09 + name: Job-{{Param.Project}}-{{Param.Version}} + parameterDefinitions: + - name: Project + type: STRING + default: TemplateDefault + - name: Version + type: STRING + default: '9.9' + steps: + - name: Step1 + script: + actions: + onRun: + command: echo + args: ["ran"] +parameters: + Project: StudioX + Version: '1.0' +expected: + output: + - anyOf: ["Job: Job-StudioX-1.0", "Running job 'Job-StudioX-1.0'"] + forbidden: + - "Job-TemplateDefault-9.9" + - "{{Param." diff --git a/conformance-tests/2023-09/base/jobs/2.3--int-param-intstring-default-resolves.test.yaml b/conformance-tests/2023-09/base/jobs/2.3--int-param-intstring-default-resolves.test.yaml new file mode 100644 index 00000000..7e5e2de1 --- /dev/null +++ b/conformance-tests/2023-09/base/jobs/2.3--int-param-intstring-default-resolves.test.yaml @@ -0,0 +1,36 @@ +# .default is ` | ` in base 2023-09 (§2.3 L306) +# with no extension gate. The string form is a distinct coercion surface: a consumer reading the +# raw template sees "007" where the resolved job carries 7. +# +# The suite has template-level coverage of the string form +# (base/job_templates/2.3--int-param-with-default-string.yaml) and no job-level coverage of any +# string-form definition field, so nothing asserts what the string resolves TO. +# +# `007` is chosen over `7` so the assertion distinguishes a parsed integer from a passed-through +# literal. `COUNT:007` is forbidden. +# +# Spec references, Template Schemas 2023-09: +# §2.3 L306 INT parameter default is ` | ` +# https://github.com/OpenJobDescription/openjd-specifications/blob/mainline/wiki/2023-09-Template-Schemas.md?plain=1#L306 +template: + specificationVersion: jobtemplate-2023-09 + name: TestJob + parameterDefinitions: + - name: Count + type: INT + default: '007' + steps: + - name: Step1 + script: + actions: + onRun: + command: python + args: + - -c + - print(r'COUNT:{{Param.Count}}') +expected: + output: + - 'COUNT:7' + forbidden: + - 'COUNT:007' + - '{{Param.' diff --git a/conformance-tests/2023-09/base/jobs/proposed/3.3.2--format-string-in-allof-resolves-to-invalid-value.invalid.test.yaml b/conformance-tests/2023-09/base/jobs/proposed/3.3.2--format-string-in-allof-resolves-to-invalid-value.invalid.test.yaml new file mode 100644 index 00000000..b1d4480c --- /dev/null +++ b/conformance-tests/2023-09/base/jobs/proposed/3.3.2--format-string-in-allof-resolves-to-invalid-value.invalid.test.yaml @@ -0,0 +1,42 @@ +# hostRequirements.attributes[].allOf is annotated @fmtstring in base 2023-09 (§3.3.2 L1015), +# the same as anyOf at §3.3.2 L1014, with no extension gate. Its element type +# is constrained to the identifier-like pattern in §3.3.2.2, and that +# pattern cannot be checked at decode when the element is a format string, so it must be +# re-checked after resolution at job creation. +# +# Here the parameter resolves to "not valid!", violating the pattern on both the space and the +# exclamation mark. +# +# Companion to 3.3.2--format-string-in-anyof-resolves-to-invalid-value.invalid.test.yaml in this +# directory. anyOf and allOf are separate spec fields with separate validators, so they are +# covered separately rather than assumed symmetric, but the observed divergence is the same and +# reviewers may prefer to treat both as one issue. +# +# Spec references, Template Schemas 2023-09: +# §3.3.2 L1015 attributes[].allOf is @fmtstring in base 2023-09, no extension gate +# https://github.com/OpenJobDescription/openjd-specifications/blob/mainline/wiki/2023-09-Template-Schemas.md?plain=1#L1015 +# §3.3.2 L1014 the anyOf counterpart, covered separately as a distinct field +# https://github.com/OpenJobDescription/openjd-specifications/blob/mainline/wiki/2023-09-Template-Schemas.md?plain=1#L1014 +# §3.3.2.2 L1059 `` constraints apply AFTER the format string resolves +# https://github.com/OpenJobDescription/openjd-specifications/blob/mainline/wiki/2023-09-Template-Schemas.md?plain=1#L1059 +template: + specificationVersion: jobtemplate-2023-09 + name: TestJob + parameterDefinitions: + - name: Feature + type: STRING + default: "not valid!" + steps: + - name: Step1 + hostRequirements: + attributes: + - name: attr.custom.features + allOf: + - '{{Param.Feature}}' + script: + actions: + onRun: + command: python + args: + - -c + - print(r'RAN') diff --git a/conformance-tests/2023-09/base/jobs/proposed/3.3.2--format-string-in-anyof-resolves-to-invalid-value.invalid.test.yaml b/conformance-tests/2023-09/base/jobs/proposed/3.3.2--format-string-in-anyof-resolves-to-invalid-value.invalid.test.yaml new file mode 100644 index 00000000..ed74fdf2 --- /dev/null +++ b/conformance-tests/2023-09/base/jobs/proposed/3.3.2--format-string-in-anyof-resolves-to-invalid-value.invalid.test.yaml @@ -0,0 +1,39 @@ +# hostRequirements.attributes[].anyOf is annotated @fmtstring in base 2023-09 (§3.3.2 L1014), +# with no extension gate. Its element type (§3.3.2.2) constrains the +# value to the identifier-like pattern, and that constraint cannot be checked at decode when the +# element is a format string. It must therefore be checked after resolution at job creation. +# +# Here the parameter resolves to "not valid!", which contains a space and an exclamation mark, +# so the run must fail. +# +# NOTE ON CURRENT STATE: this case is expected to FAIL against the Rust CLI, which accepts the +# resolved value and runs the job. The Python CLI rejects it with "Value not valid! is not a +# valid attribute capability value." That divergence is the defect this case exists to expose; +# it is not a broken test. +# +# Spec references, Template Schemas 2023-09: +# §3.3.2 L1014 attributes[].anyOf is @fmtstring in base 2023-09, no extension gate +# https://github.com/OpenJobDescription/openjd-specifications/blob/mainline/wiki/2023-09-Template-Schemas.md?plain=1#L1014 +# §3.3.2.2 L1059 `` constraints apply AFTER the format string resolves +# https://github.com/OpenJobDescription/openjd-specifications/blob/mainline/wiki/2023-09-Template-Schemas.md?plain=1#L1059 +template: + specificationVersion: jobtemplate-2023-09 + name: TestJob + parameterDefinitions: + - name: Software + type: STRING + default: "not valid!" + steps: + - name: Step1 + hostRequirements: + attributes: + - name: attr.custom.software + anyOf: + - '{{Param.Software}}' + script: + actions: + onRun: + command: python + args: + - -c + - print(r'RAN') diff --git a/conformance-tests/2023-09/base/jobs/proposed/3.4.1.1--int-range-intstring-elements-normalized.test.yaml b/conformance-tests/2023-09/base/jobs/proposed/3.4.1.1--int-range-intstring-elements-normalized.test.yaml new file mode 100644 index 00000000..203e4897 --- /dev/null +++ b/conformance-tests/2023-09/base/jobs/proposed/3.4.1.1--int-range-intstring-elements-normalized.test.yaml @@ -0,0 +1,41 @@ +# elements are ` | ` in base 2023-09 (Template Schemas +# §3.4.1.1 L1110), with no extension gate. The suite has 2,417 IntRangeList elements and not one +# uses the string form, so nothing asserts what a string element resolves to. +# +# is defined as "a string whose value is the string representation of an integer +# value in base-10", so '02' and '003' represent the integers 2 and 3, and the task parameter +# should carry those integers rather than the source text. +# +# This is the value that reaches a task command line, so a wrong answer here means a renderer is +# invoked with --frame 02 instead of --frame 2. +# +# Spec references, Template Schemas 2023-09: +# §3.4.1.1 L1110 `` elements are ` | ` +# https://github.com/OpenJobDescription/openjd-specifications/blob/mainline/wiki/2023-09-Template-Schemas.md?plain=1#L1110 +# §2.3 L318 `` is a string representation of an integer in base-10 +# https://github.com/OpenJobDescription/openjd-specifications/blob/mainline/wiki/2023-09-Template-Schemas.md?plain=1#L318 +template: + specificationVersion: jobtemplate-2023-09 + name: TestJob + steps: + - name: Step1 + parameterSpace: + taskParameterDefinitions: + - name: Frame + type: INT + range: ['1', '02', '003'] + script: + actions: + onRun: + command: python + args: + - -c + - print(r'FRAME:{{Task.Param.Frame}}') +expected: + output: + - 'FRAME:1' + - 'FRAME:2' + - 'FRAME:3' + forbidden: + - 'FRAME:02' + - 'FRAME:003' diff --git a/conformance-tests/2023-09/base/jobs/proposed/3.4.1.2--float-range-floatstring-elements-normalized.test.yaml b/conformance-tests/2023-09/base/jobs/proposed/3.4.1.2--float-range-floatstring-elements-normalized.test.yaml new file mode 100644 index 00000000..1c090bff --- /dev/null +++ b/conformance-tests/2023-09/base/jobs/proposed/3.4.1.2--float-range-floatstring-elements-normalized.test.yaml @@ -0,0 +1,45 @@ +# elements are ` | ` in base 2023-09 (Template Schemas +# §3.4.1.2 L1184), with no extension gate. The suite has 1,044 FloatRangeList elements and not +# one uses the string form. +# +# Same question as the IntRangeList case in this directory: is a string +# representation of a base-10 number, so '02.50' represents 2.5 and the task parameter should +# carry that number rather than the source text. +# +# Note for reviewers: the direction of normalization is not consistent across the two +# implementations today, and it is not consistent across FIELDS either. For range-list elements +# one implementation preserves the literal and the other normalizes; for a FLOAT parameter +# DEFAULT the two swap sides. An already-landed fixture, +# EXPR/jobs/expr1.3.4--float-passthrough.test.yaml, pins the verbatim behaviour for a parameter +# default by asserting PARAM:3.500 from default "3.500". So a spec ruling is needed on whether +# normalizes, and if it does, the landed fixture and this one cannot both be +# right. This fixture states the normalizing reading; it should not merge until that is settled. +# +# Spec references, Template Schemas 2023-09: +# §3.4.1.2 L1184 `` elements are ` | ` +# https://github.com/OpenJobDescription/openjd-specifications/blob/mainline/wiki/2023-09-Template-Schemas.md?plain=1#L1184 +# §2.4 L375 `` is a string representation of a float or integer in base-10 +# https://github.com/OpenJobDescription/openjd-specifications/blob/mainline/wiki/2023-09-Template-Schemas.md?plain=1#L375 +template: + specificationVersion: jobtemplate-2023-09 + name: TestJob + steps: + - name: Step1 + parameterSpace: + taskParameterDefinitions: + - name: Weight + type: FLOAT + range: ['1.5', '02.50'] + script: + actions: + onRun: + command: python + args: + - -c + - print(r'W:{{Task.Param.Weight}}') +expected: + output: + - 'W:1.5' + - 'W:2.5' + forbidden: + - 'W:02.50' diff --git a/conformance-tests/2023-09/base/jobs/proposed/README.md b/conformance-tests/2023-09/base/jobs/proposed/README.md new file mode 100644 index 00000000..4518c24d --- /dev/null +++ b/conformance-tests/2023-09/base/jobs/proposed/README.md @@ -0,0 +1,73 @@ +# Proposed base job fixtures: post-resolution constraint checks + +These fixtures are believed spec-correct and currently fail at least one reference +implementation. They live here rather than in `../` because the runner discovers test +files with a non-recursive glob, so `proposed/` is excluded and the suite stays green. +Promotion is `git mv` up one directory with no edit to the fixture. + +| Fixture | Construct | Observed | +|---|---|---| +| `3.3.2--format-string-in-anyof-resolves-to-invalid-value.invalid.test.yaml` | `attributes[].anyOf` element written as `"{{Param.Software}}"`, resolving to `not valid!`, against the `` pattern in section 3.3.2.2 | Python rejects at job creation with `Value not valid! is not a valid attribute capability value.` The Rust CLI accepts the resolved value and runs the job | +| `3.3.2--format-string-in-allof-resolves-to-invalid-value.invalid.test.yaml` | Same, for `allOf` at §3.3.2 L1015 | Same. Swept across four invalid resolved values, a space, an exclamation mark, 120 characters and a leading digit: Python rejects all four, Rust runs all four | +| `3.4.1.1--int-range-intstring-elements-normalized.test.yaml` | `` elements in the `` string form, `['1', '02', '003']` | Rust substitutes `1`, `2`, `3`. Python substitutes `1`, `02`, `003`, so a task command line receives `--frame 02` | +| `3.4.1.2--float-range-floatstring-elements-normalized.test.yaml` | `` elements in the `` string form, `['1.5', '02.50']` | Rust substitutes `1.5`, `2.5`. Python substitutes `1.5`, `02.50` | + +## Classification: the two capability-value fixtures + +Implementation fix. + +`anyOf` is annotated `@fmtstring` in base 2023-09 at (§3.3.2 L1014, with no +extension gate, and its element type is constrained to the identifier-like pattern in +section 3.3.2.2. That pattern cannot be checked at decode when the element is a format +string, so it must be re-checked after resolution at job creation. The Python path does +this; the Rust path defers the check and never resumes it. + +The value used here, `not valid!`, violates the pattern twice, on the space and on the +exclamation mark, so no reading of the pattern admits it. + +This is the same defect shape as +`../../../TASK_CHUNKING/jobs/proposed/default-task-count-format-string-resolves-to-zero.invalid.test.yaml`: +validation correctly deferred for a format string, never resumed. Reviewers may prefer +to treat the two as one issue. + +Not covered by any fixture, here or in `../`: the positive case asserting that a +resolved `anyOf` value is *correct*. No `openjd` CLI surfaces resolved host +requirements, in `summary --output json` or anywhere else, and the runner asserts only +on stdout and task status, so the resolved value has no observable effect on a +single-host run. The negative case above is the reachable half. + + +## Classification: the two range-element fixtures + +Spec decision needed, then an implementation fix on one side or the other. + +`` and `` are defined only as "a string whose value is the string +representation of an integer / floating point value in base-10". Nothing is said about +whether the string form normalizes, so `['02']` yielding `2` and yielding `02` are both +defensible readings of the text as written. + +Two facts make this worth a ruling rather than a shrug. First, the value reaches a task +command line, so the two readings produce different renderer invocations for the same +template. Second, the implementations do not merely disagree, they disagree in opposite +directions on different fields: for range-list elements Python preserves the literal and +Rust normalizes, while for a FLOAT parameter `default` Python normalizes and Rust preserves. + +There is also a conflict with a landed fixture. `EXPR/jobs/expr1.3.4--float-passthrough.test.yaml` +asserts `PARAM:3.500` from `default: "3.500"`, which pins the verbatim reading for a parameter +default. If the spec rules that `` normalizes, that fixture and +`3.4.1.2--...` here cannot both be correct. + +These two fixtures state the normalizing reading. They should not be promoted out of +`proposed/` until the spec says which reading is conformant. + +## Spec references, Template Schemas 2023-09 + +- [§3.3.2 L1014](https://github.com/OpenJobDescription/openjd-specifications/blob/mainline/wiki/2023-09-Template-Schemas.md?plain=1#L1014) attributes[].anyOf is @fmtstring in base 2023-09, no extension gate +- [§3.3.2 L1015](https://github.com/OpenJobDescription/openjd-specifications/blob/mainline/wiki/2023-09-Template-Schemas.md?plain=1#L1015) attributes[].allOf, same +- [§3.3.2.2 L1059](https://github.com/OpenJobDescription/openjd-specifications/blob/mainline/wiki/2023-09-Template-Schemas.md?plain=1#L1059) `` constraints apply AFTER the format string resolves +- [§3.4.1.1 L1110](https://github.com/OpenJobDescription/openjd-specifications/blob/mainline/wiki/2023-09-Template-Schemas.md?plain=1#L1110) `` elements are ` | ` +- [§3.4.1.2 L1184](https://github.com/OpenJobDescription/openjd-specifications/blob/mainline/wiki/2023-09-Template-Schemas.md?plain=1#L1184) `` elements are ` | ` +- [§2.3 L318](https://github.com/OpenJobDescription/openjd-specifications/blob/mainline/wiki/2023-09-Template-Schemas.md?plain=1#L318) `` is a string representation of an integer in base-10 + +Line numbers are a locator for where each claim was verified. The section numbers are the +durable reference if the spec is re-flowed.