diff --git a/conformance/blueprint/v1/cases.json b/conformance/blueprint/v1/cases.json
index 995b806..650580d 100644
--- a/conformance/blueprint/v1/cases.json
+++ b/conformance/blueprint/v1/cases.json
@@ -137,6 +137,21 @@
"phase": "structural",
"path": "structural/023-generated-parameter-marked-insensitive"
},
+ {
+ "id": "structural-024-integer-parameter-type",
+ "phase": "structural",
+ "path": "structural/024-integer-parameter-type"
+ },
+ {
+ "id": "structural-025-json-parameter-with-pattern",
+ "phase": "structural",
+ "path": "structural/025-json-parameter-with-pattern"
+ },
+ {
+ "id": "structural-026-json-parameter",
+ "phase": "structural",
+ "path": "structural/026-json-parameter"
+ },
{
"id": "semantic-001-connection-names-unknown-role",
"phase": "semantic",
diff --git a/conformance/blueprint/v1/semantic/009-conflicting-input-schema/tree/acme-wiki/blueprint.yaml b/conformance/blueprint/v1/semantic/009-conflicting-input-schema/tree/acme-wiki/blueprint.yaml
index 32e9375..de11ccf 100644
--- a/conformance/blueprint/v1/semantic/009-conflicting-input-schema/tree/acme-wiki/blueprint.yaml
+++ b/conformance/blueprint/v1/semantic/009-conflicting-input-schema/tree/acme-wiki/blueprint.yaml
@@ -1,5 +1,5 @@
# Both components declare `adminPassword`, and they disagree about what it is:
-# a STRING for `api`, an INTEGER for `db`. Silent first-wins would hand `db` a
+# a STRING for `api`, a NUMBER for `db`. Silent first-wins would hand `db` a
# value validated against `api`'s rules and fail at deploy time, a long way from
# either document.
#
diff --git a/conformance/blueprint/v1/semantic/009-conflicting-input-schema/tree/acme-wiki/components/postgres.yaml b/conformance/blueprint/v1/semantic/009-conflicting-input-schema/tree/acme-wiki/components/postgres.yaml
index 28b0a7a..754b8c1 100644
--- a/conformance/blueprint/v1/semantic/009-conflicting-input-schema/tree/acme-wiki/components/postgres.yaml
+++ b/conformance/blueprint/v1/semantic/009-conflicting-input-schema/tree/acme-wiki/components/postgres.yaml
@@ -17,7 +17,7 @@ spec:
inputs:
adminPassword:
schema:
- type: INTEGER
+ type: NUMBER
isSensitive: true
suppliedBy: USER
ui:
diff --git a/conformance/blueprint/v1/semantic/012-connection-type-mismatch/tree/acme-wiki/blueprint.yaml b/conformance/blueprint/v1/semantic/012-connection-type-mismatch/tree/acme-wiki/blueprint.yaml
index 06b5a3e..272d048 100644
--- a/conformance/blueprint/v1/semantic/012-connection-type-mismatch/tree/acme-wiki/blueprint.yaml
+++ b/conformance/blueprint/v1/semantic/012-connection-type-mismatch/tree/acme-wiki/blueprint.yaml
@@ -1,4 +1,4 @@
-# `db` publishes an INTEGER port; `web` asks for a STRING. Both ends resolve —
+# `db` publishes a NUMBER port; `web` asks for a STRING. Both ends resolve —
# the node, the output and the input all exist — so nothing above §4.2's
# compatibility rule fires. Without it the mismatch reaches the consuming
# workload at deploy time with nothing pointing back at either document.
diff --git a/conformance/blueprint/v1/semantic/012-connection-type-mismatch/tree/acme-wiki/components/postgres.yaml b/conformance/blueprint/v1/semantic/012-connection-type-mismatch/tree/acme-wiki/components/postgres.yaml
index 4e7060d..1f0b613 100644
--- a/conformance/blueprint/v1/semantic/012-connection-type-mismatch/tree/acme-wiki/components/postgres.yaml
+++ b/conformance/blueprint/v1/semantic/012-connection-type-mismatch/tree/acme-wiki/components/postgres.yaml
@@ -18,6 +18,6 @@ spec:
outputs:
port:
schema:
- type: INTEGER
+ type: NUMBER
valueFrom: DECLARED
value: '5432'
diff --git a/conformance/blueprint/v1/semantic/019-parameter-type-disagrees/tree/acme-wiki/blueprint.yaml b/conformance/blueprint/v1/semantic/019-parameter-type-disagrees/tree/acme-wiki/blueprint.yaml
index cbe8d4a..2244005 100644
--- a/conformance/blueprint/v1/semantic/019-parameter-type-disagrees/tree/acme-wiki/blueprint.yaml
+++ b/conformance/blueprint/v1/semantic/019-parameter-type-disagrees/tree/acme-wiki/blueprint.yaml
@@ -1,4 +1,4 @@
-# The parameter asks for an INTEGER; the input it covers declares a STRING.
+# The parameter asks for a NUMBER; the input it covers declares a STRING.
# The deploying user is validated against the parameter's schema and the
# component then receives the result against its own, so nothing fails until the
# workload starts.
@@ -20,7 +20,7 @@ spec:
parameters:
adminPassword:
schema:
- type: INTEGER
+ type: NUMBER
isSensitive: true
isRequired: true
ui:
diff --git a/conformance/blueprint/v1/structural/024-integer-parameter-type/case.yaml b/conformance/blueprint/v1/structural/024-integer-parameter-type/case.yaml
new file mode 100644
index 0000000..42ce15a
--- /dev/null
+++ b/conformance/blueprint/v1/structural/024-integer-parameter-type/case.yaml
@@ -0,0 +1,22 @@
+# The component half of this rule is component structural/039. This family
+# carries its own parameter value schema, so a fixture over there proves nothing
+# about a blueprint — the two would drift apart silently and CI would stay
+# green.
+specVersion: v1
+kind: BLUEPRINT
+metadata:
+ slug: acme-wiki
+ version: 1
+spec:
+ components:
+ db:
+ component: ./components/postgres.yaml
+ size: general.standard.small
+ connections: {}
+ parameters:
+ replicaCount:
+ schema:
+ type: INTEGER
+ isRequired: true
+ ui:
+ label: Replica count
diff --git a/conformance/blueprint/v1/structural/024-integer-parameter-type/diagnostics.json b/conformance/blueprint/v1/structural/024-integer-parameter-type/diagnostics.json
new file mode 100644
index 0000000..12cef02
--- /dev/null
+++ b/conformance/blueprint/v1/structural/024-integer-parameter-type/diagnostics.json
@@ -0,0 +1,6 @@
+[
+ {
+ "code": "ERR_INVALID_VALUE",
+ "path": "/spec/parameters/replicaCount/schema/type"
+ }
+]
diff --git a/conformance/blueprint/v1/structural/024-integer-parameter-type/metadata.json b/conformance/blueprint/v1/structural/024-integer-parameter-type/metadata.json
new file mode 100644
index 0000000..3302dbc
--- /dev/null
+++ b/conformance/blueprint/v1/structural/024-integer-parameter-type/metadata.json
@@ -0,0 +1,8 @@
+{
+ "id": "structural-024-integer-parameter-type",
+ "phase": "structural",
+ "expected": "fail",
+ "clause": "specifications/blueprint/v1/spec.md#authored-parameters",
+ "requirements": ["COMP-VAL-001"],
+ "summary": "An authored parameter declaring the withdrawn INTEGER shape is rejected."
+}
diff --git a/conformance/blueprint/v1/structural/025-json-parameter-with-pattern/case.yaml b/conformance/blueprint/v1/structural/025-json-parameter-with-pattern/case.yaml
new file mode 100644
index 0000000..5fbede2
--- /dev/null
+++ b/conformance/blueprint/v1/structural/025-json-parameter-with-pattern/case.yaml
@@ -0,0 +1,22 @@
+# The JSON restrictions belong to component §6.3 and this family enforces them
+# on the same terms. Fixtured here because the enforcement is a separate schema
+# module, not because the rule is a separate rule.
+specVersion: v1
+kind: BLUEPRINT
+metadata:
+ slug: acme-wiki
+ version: 1
+spec:
+ components:
+ db:
+ component: ./components/postgres.yaml
+ size: general.standard.small
+ connections: {}
+ parameters:
+ settings:
+ schema:
+ type: JSON
+ pattern: '^\{.*\}$'
+ isRequired: false
+ ui:
+ label: Advanced settings
diff --git a/conformance/blueprint/v1/structural/025-json-parameter-with-pattern/diagnostics.json b/conformance/blueprint/v1/structural/025-json-parameter-with-pattern/diagnostics.json
new file mode 100644
index 0000000..bc7bd18
--- /dev/null
+++ b/conformance/blueprint/v1/structural/025-json-parameter-with-pattern/diagnostics.json
@@ -0,0 +1,6 @@
+[
+ {
+ "code": "ERR_INVALID_TYPE",
+ "path": "/spec/parameters/settings/schema/pattern"
+ }
+]
diff --git a/conformance/blueprint/v1/structural/025-json-parameter-with-pattern/metadata.json b/conformance/blueprint/v1/structural/025-json-parameter-with-pattern/metadata.json
new file mode 100644
index 0000000..0041493
--- /dev/null
+++ b/conformance/blueprint/v1/structural/025-json-parameter-with-pattern/metadata.json
@@ -0,0 +1,8 @@
+{
+ "id": "structural-025-json-parameter-with-pattern",
+ "phase": "structural",
+ "expected": "fail",
+ "clause": "specifications/blueprint/v1/spec.md#authored-parameters",
+ "requirements": ["COMP-VAL-002"],
+ "summary": "A regular expression on a JSON parameter is rejected."
+}
diff --git a/conformance/blueprint/v1/structural/026-json-parameter/case.yaml b/conformance/blueprint/v1/structural/026-json-parameter/case.yaml
new file mode 100644
index 0000000..621733d
--- /dev/null
+++ b/conformance/blueprint/v1/structural/026-json-parameter/case.yaml
@@ -0,0 +1,21 @@
+# The positive verdict for the same module, so both branches of the conditional
+# are exercised in this family and not only in the component one.
+specVersion: v1
+kind: BLUEPRINT
+metadata:
+ slug: acme-wiki
+ version: 1
+spec:
+ components:
+ db:
+ component: ./components/postgres.yaml
+ size: general.standard.small
+ connections: {}
+ parameters:
+ settings:
+ schema:
+ type: JSON
+ default: '{"logLevel":"info"}'
+ isRequired: false
+ ui:
+ label: Advanced settings
diff --git a/conformance/blueprint/v1/structural/026-json-parameter/metadata.json b/conformance/blueprint/v1/structural/026-json-parameter/metadata.json
new file mode 100644
index 0000000..e88159f
--- /dev/null
+++ b/conformance/blueprint/v1/structural/026-json-parameter/metadata.json
@@ -0,0 +1,8 @@
+{
+ "id": "structural-026-json-parameter",
+ "phase": "structural",
+ "expected": "pass",
+ "clause": "specifications/blueprint/v1/spec.md#authored-parameters",
+ "requirements": ["COMP-VAL-001"],
+ "summary": "An authored parameter may take a JSON value."
+}
diff --git a/conformance/component/v1/cases.json b/conformance/component/v1/cases.json
index c32d09d..c71c236 100644
--- a/conformance/component/v1/cases.json
+++ b/conformance/component/v1/cases.json
@@ -257,6 +257,36 @@
"phase": "structural",
"path": "structural/038-probe-defaults-are-effective"
},
+ {
+ "id": "structural-039-integer-value-type",
+ "phase": "structural",
+ "path": "structural/039-integer-value-type"
+ },
+ {
+ "id": "structural-040-number-value-type",
+ "phase": "structural",
+ "path": "structural/040-number-value-type"
+ },
+ {
+ "id": "structural-041-json-value-type",
+ "phase": "structural",
+ "path": "structural/041-json-value-type"
+ },
+ {
+ "id": "structural-042-json-value-with-pattern",
+ "phase": "structural",
+ "path": "structural/042-json-value-with-pattern"
+ },
+ {
+ "id": "structural-043-json-value-with-enum",
+ "phase": "structural",
+ "path": "structural/043-json-value-with-enum"
+ },
+ {
+ "id": "structural-044-format-on-a-non-string-value",
+ "phase": "structural",
+ "path": "structural/044-format-on-a-non-string-value"
+ },
{
"id": "semantic-001-floating-tag-image-reference",
"phase": "semantic",
diff --git a/conformance/component/v1/structural/036-public-port-on-udp-endpoint/case.yaml b/conformance/component/v1/structural/036-public-port-on-udp-endpoint/case.yaml
index 9ee2bc6..33a5745 100644
--- a/conformance/component/v1/structural/036-public-port-on-udp-endpoint/case.yaml
+++ b/conformance/component/v1/structural/036-public-port-on-udp-endpoint/case.yaml
@@ -21,7 +21,7 @@ spec:
inputs:
gamePort:
schema:
- type: INTEGER
+ type: NUMBER
ui:
label: Game server port
platformDefault:
diff --git a/conformance/component/v1/structural/039-integer-value-type/case.yaml b/conformance/component/v1/structural/039-integer-value-type/case.yaml
new file mode 100644
index 0000000..c612afa
--- /dev/null
+++ b/conformance/component/v1/structural/039-integer-value-type/case.yaml
@@ -0,0 +1,26 @@
+# INTEGER was a member of this vocabulary and is not one now. The case exists to
+# make the withdrawal executable rather than declared: it passed before NUMBER
+# replaced INTEGER, and fails after. See ADR 0013.
+specVersion: v1
+kind: COMPONENT
+metadata:
+ version: 1
+spec:
+ workload:
+ kind: SERVICE
+ source:
+ type: IMAGE
+ ref: nginx:1.29.4-alpine
+ endpoints:
+ web:
+ containerPort: 8080
+ protocol: HTTP
+ visibility: PRIVATE
+ contract:
+ inputs:
+ replicaCount:
+ schema:
+ type: INTEGER
+ ui:
+ label: Replica count
+ outputs: {}
diff --git a/conformance/component/v1/structural/039-integer-value-type/diagnostics.json b/conformance/component/v1/structural/039-integer-value-type/diagnostics.json
new file mode 100644
index 0000000..bf6f765
--- /dev/null
+++ b/conformance/component/v1/structural/039-integer-value-type/diagnostics.json
@@ -0,0 +1,6 @@
+[
+ {
+ "code": "ERR_INVALID_VALUE",
+ "path": "/spec/contract/inputs/replicaCount/schema/type"
+ }
+]
diff --git a/conformance/component/v1/structural/039-integer-value-type/metadata.json b/conformance/component/v1/structural/039-integer-value-type/metadata.json
new file mode 100644
index 0000000..67703e0
--- /dev/null
+++ b/conformance/component/v1/structural/039-integer-value-type/metadata.json
@@ -0,0 +1,8 @@
+{
+ "id": "structural-039-integer-value-type",
+ "phase": "structural",
+ "expected": "fail",
+ "clause": "specifications/component/v1/spec.md#value-schema",
+ "requirements": ["COMP-VAL-001"],
+ "summary": "A value schema declaring the withdrawn INTEGER shape is rejected."
+}
diff --git a/conformance/component/v1/structural/040-number-value-type/case.yaml b/conformance/component/v1/structural/040-number-value-type/case.yaml
new file mode 100644
index 0000000..faa35ae
--- /dev/null
+++ b/conformance/component/v1/structural/040-number-value-type/case.yaml
@@ -0,0 +1,34 @@
+# NUMBER is JSON's own numeric kind, so it covers whole numbers and reals alike.
+# An author who needs whole numbers writes the constraint rather than reaching
+# for a second type, and this is what that looks like. The output's `value` is a
+# quoted string because every value is carried as text — the shape names the
+# string form, not a host type.
+specVersion: v1
+kind: COMPONENT
+metadata:
+ version: 1
+spec:
+ workload:
+ kind: SERVICE
+ source:
+ type: IMAGE
+ ref: postgres:17.10-alpine
+ endpoints:
+ primary:
+ containerPort: 5432
+ protocol: TCP
+ visibility: PRIVATE
+ contract:
+ inputs:
+ port:
+ schema:
+ type: NUMBER
+ pattern: '^-?[0-9]+$'
+ ui:
+ label: Listen port
+ outputs:
+ port:
+ schema:
+ type: NUMBER
+ valueFrom: DECLARED
+ value: '5432'
diff --git a/conformance/component/v1/structural/040-number-value-type/metadata.json b/conformance/component/v1/structural/040-number-value-type/metadata.json
new file mode 100644
index 0000000..6e479dd
--- /dev/null
+++ b/conformance/component/v1/structural/040-number-value-type/metadata.json
@@ -0,0 +1,13 @@
+{
+ "id": "structural-040-number-value-type",
+ "phase": "structural",
+ "expected": "pass",
+ "clause": "specifications/component/v1/spec.md#value-schema",
+ "requirements": ["COMP-VAL-001"],
+ "summary": "A NUMBER value constrained to whole numbers by a pattern validates.",
+ "effective": {
+ "/spec/contract/inputs/port/schema/format": null,
+ "/spec/contract/inputs/port/schema/isSensitive": false,
+ "/spec/contract/inputs/port/schema/semanticType": null
+ }
+}
diff --git a/conformance/component/v1/structural/041-json-value-type/case.yaml b/conformance/component/v1/structural/041-json-value-type/case.yaml
new file mode 100644
index 0000000..d090b42
--- /dev/null
+++ b/conformance/component/v1/structural/041-json-value-type/case.yaml
@@ -0,0 +1,34 @@
+# A JSON value's string form is a JSON document, so its `default` is a string
+# whose content is that document — quoted, or the YAML profile would read it as
+# a flow mapping and reject it against `type: string`. Nothing in any phase
+# parses it: the contract does not check a value against the shape beside it,
+# and that is as true of JSON as of every other member.
+specVersion: v1
+kind: COMPONENT
+metadata:
+ version: 1
+spec:
+ workload:
+ kind: SERVICE
+ source:
+ type: IMAGE
+ ref: nginx:1.29.4-alpine
+ endpoints:
+ web:
+ containerPort: 8080
+ protocol: HTTP
+ visibility: PRIVATE
+ contract:
+ inputs:
+ settings:
+ schema:
+ type: JSON
+ default: '{"logLevel":"info"}'
+ ui:
+ label: Advanced settings
+ outputs:
+ topology:
+ schema:
+ type: JSON
+ valueFrom: DECLARED
+ value: '{"shards":1}'
diff --git a/conformance/component/v1/structural/041-json-value-type/metadata.json b/conformance/component/v1/structural/041-json-value-type/metadata.json
new file mode 100644
index 0000000..c1f4468
--- /dev/null
+++ b/conformance/component/v1/structural/041-json-value-type/metadata.json
@@ -0,0 +1,12 @@
+{
+ "id": "structural-041-json-value-type",
+ "phase": "structural",
+ "expected": "pass",
+ "clause": "specifications/component/v1/spec.md#value-schema",
+ "requirements": ["COMP-VAL-001"],
+ "summary": "A JSON value carries its default as JSON text.",
+ "effective": {
+ "/spec/contract/inputs/settings/schema/pattern": null,
+ "/spec/contract/inputs/settings/schema/format": null
+ }
+}
diff --git a/conformance/component/v1/structural/042-json-value-with-pattern/case.yaml b/conformance/component/v1/structural/042-json-value-with-pattern/case.yaml
new file mode 100644
index 0000000..40fae09
--- /dev/null
+++ b/conformance/component/v1/structural/042-json-value-with-pattern/case.yaml
@@ -0,0 +1,28 @@
+# One JSON value has many spellings, and a regular expression decides membership
+# on the spelling. A rule written with one would accept one author's formatter
+# and reject another's, so the pairing is rejected outright rather than left to
+# mean whatever each implementation's serializer happens to emit.
+specVersion: v1
+kind: COMPONENT
+metadata:
+ version: 1
+spec:
+ workload:
+ kind: SERVICE
+ source:
+ type: IMAGE
+ ref: nginx:1.29.4-alpine
+ endpoints:
+ web:
+ containerPort: 8080
+ protocol: HTTP
+ visibility: PRIVATE
+ contract:
+ inputs:
+ settings:
+ schema:
+ type: JSON
+ pattern: '^\{.*\}$'
+ ui:
+ label: Advanced settings
+ outputs: {}
diff --git a/conformance/component/v1/structural/042-json-value-with-pattern/diagnostics.json b/conformance/component/v1/structural/042-json-value-with-pattern/diagnostics.json
new file mode 100644
index 0000000..ae631c1
--- /dev/null
+++ b/conformance/component/v1/structural/042-json-value-with-pattern/diagnostics.json
@@ -0,0 +1,6 @@
+[
+ {
+ "code": "ERR_INVALID_TYPE",
+ "path": "/spec/contract/inputs/settings/schema/pattern"
+ }
+]
diff --git a/conformance/component/v1/structural/042-json-value-with-pattern/metadata.json b/conformance/component/v1/structural/042-json-value-with-pattern/metadata.json
new file mode 100644
index 0000000..f539395
--- /dev/null
+++ b/conformance/component/v1/structural/042-json-value-with-pattern/metadata.json
@@ -0,0 +1,8 @@
+{
+ "id": "structural-042-json-value-with-pattern",
+ "phase": "structural",
+ "expected": "fail",
+ "clause": "specifications/component/v1/spec.md#value-schema",
+ "requirements": ["COMP-VAL-002"],
+ "summary": "A regular expression on a JSON value is rejected."
+}
diff --git a/conformance/component/v1/structural/043-json-value-with-enum/case.yaml b/conformance/component/v1/structural/043-json-value-with-enum/case.yaml
new file mode 100644
index 0000000..a3658af
--- /dev/null
+++ b/conformance/component/v1/structural/043-json-value-with-enum/case.yaml
@@ -0,0 +1,31 @@
+# The companion to 042, and a separate case because the two halves of the rule
+# carry different codes. An empty `enum` is the unconstrained state, so the
+# restriction is that the list stays empty rather than that the field is absent.
+# The two members below are the same JSON value spelled two ways, which is the
+# whole argument in one document.
+specVersion: v1
+kind: COMPONENT
+metadata:
+ version: 1
+spec:
+ workload:
+ kind: SERVICE
+ source:
+ type: IMAGE
+ ref: nginx:1.29.4-alpine
+ endpoints:
+ web:
+ containerPort: 8080
+ protocol: HTTP
+ visibility: PRIVATE
+ contract:
+ inputs:
+ settings:
+ schema:
+ type: JSON
+ enum:
+ - '{"a":1}'
+ - '{ "a" : 1 }'
+ ui:
+ label: Advanced settings
+ outputs: {}
diff --git a/conformance/component/v1/structural/043-json-value-with-enum/diagnostics.json b/conformance/component/v1/structural/043-json-value-with-enum/diagnostics.json
new file mode 100644
index 0000000..d605b15
--- /dev/null
+++ b/conformance/component/v1/structural/043-json-value-with-enum/diagnostics.json
@@ -0,0 +1,6 @@
+[
+ {
+ "code": "ERR_INVALID_VALUE",
+ "path": "/spec/contract/inputs/settings/schema/enum"
+ }
+]
diff --git a/conformance/component/v1/structural/043-json-value-with-enum/metadata.json b/conformance/component/v1/structural/043-json-value-with-enum/metadata.json
new file mode 100644
index 0000000..58a5229
--- /dev/null
+++ b/conformance/component/v1/structural/043-json-value-with-enum/metadata.json
@@ -0,0 +1,8 @@
+{
+ "id": "structural-043-json-value-with-enum",
+ "phase": "structural",
+ "expected": "fail",
+ "clause": "specifications/component/v1/spec.md#value-schema",
+ "requirements": ["COMP-VAL-002"],
+ "summary": "A closed enumeration on a JSON value is rejected."
+}
diff --git a/conformance/component/v1/structural/044-format-on-a-non-string-value/case.yaml b/conformance/component/v1/structural/044-format-on-a-non-string-value/case.yaml
new file mode 100644
index 0000000..dea03d7
--- /dev/null
+++ b/conformance/component/v1/structural/044-format-on-a-non-string-value/case.yaml
@@ -0,0 +1,28 @@
+# Every format member names a lexical convention for text, so the field refines
+# a STRING and nothing else. URI on a NUMBER describes nothing; that it used to
+# validate was an accident of the two fields never having been described
+# together.
+specVersion: v1
+kind: COMPONENT
+metadata:
+ version: 1
+spec:
+ workload:
+ kind: SERVICE
+ source:
+ type: IMAGE
+ ref: nginx:1.29.4-alpine
+ endpoints:
+ web:
+ containerPort: 8080
+ protocol: HTTP
+ visibility: PRIVATE
+ contract:
+ inputs:
+ retries:
+ schema:
+ type: NUMBER
+ format: URI
+ ui:
+ label: Retry count
+ outputs: {}
diff --git a/conformance/component/v1/structural/044-format-on-a-non-string-value/diagnostics.json b/conformance/component/v1/structural/044-format-on-a-non-string-value/diagnostics.json
new file mode 100644
index 0000000..e8861ed
--- /dev/null
+++ b/conformance/component/v1/structural/044-format-on-a-non-string-value/diagnostics.json
@@ -0,0 +1,6 @@
+[
+ {
+ "code": "ERR_INVALID_TYPE",
+ "path": "/spec/contract/inputs/retries/schema/format"
+ }
+]
diff --git a/conformance/component/v1/structural/044-format-on-a-non-string-value/metadata.json b/conformance/component/v1/structural/044-format-on-a-non-string-value/metadata.json
new file mode 100644
index 0000000..85379f9
--- /dev/null
+++ b/conformance/component/v1/structural/044-format-on-a-non-string-value/metadata.json
@@ -0,0 +1,8 @@
+{
+ "id": "structural-044-format-on-a-non-string-value",
+ "phase": "structural",
+ "expected": "fail",
+ "clause": "specifications/component/v1/spec.md#value-schema",
+ "requirements": ["COMP-VAL-003"],
+ "summary": "A format refinement on a value that is not text is rejected."
+}
diff --git a/docs/adr/0013-value-shape-vocabulary.md b/docs/adr/0013-value-shape-vocabulary.md
new file mode 100644
index 0000000..09afddb
--- /dev/null
+++ b/docs/adr/0013-value-shape-vocabulary.md
@@ -0,0 +1,188 @@
+# ADR 0013: The value-shape vocabulary, and what `JSON` means in it
+
+- **Status:** Accepted
+- **Date:** 2026-08-19
+- **Extends:** [ADR 0003](0003-controlled-vocabulary-placement.md) §1
+- **Relies on:** [ADR 0005](0005-platform-divergence-reconciliation.md) §1
+
+## Context
+
+An input's `schema.type` and a blueprint parameter's `schema.type` name the
+**shape** of a configuration value. Until this ADR the vocabulary was `STRING`,
+`INTEGER`, `BOOLEAN`. Three things about that are worth stating together,
+because no one of them would have been worth a decision on its own.
+
+**The vocabulary was published nowhere.** Neither `spec.md` enumerated the
+members. Component §6.1 and §6.2 discussed `schema` and `type` at length without
+ever saying what `type` may contain, while every neighbouring vocabulary —
+`suppliedBy`, `platformDefault.source`, `valueFrom` — got a prose table. The only
+statement of membership anywhere was a schema `description` string, and
+[CONTRIBUTING](../../.github/CONTRIBUTING.md) ground rule 1 makes `description`
+fields *informative*. So the meaning of `type` was defined in a field this
+repository declares non-normative. That is survivable for three self-evident
+primitives and stops being survivable the moment a member arrives whose meaning
+is not self-evident.
+
+**The platform diverged.** `musher-dev/platform` PR #1997 adopted `STRING`,
+`NUMBER`, `BOOLEAN`, `JSON` — JSON's own value kinds — and filed
+[issue #54](https://github.com/musher-dev/spec/issues/54) against this
+repository asking for two conformance fixtures to be renamed. The issue had the
+divergence inverted: it cited a "§10" defining the new vocabulary, and no such
+section exists here. Applying the rename as filed would have broken this
+repository's CI in exactly the two ways the issue described, mirrored — a `pass`
+fixture failing structurally on a token the schema does not admit, and a
+`semantic` mismatch fixture dying before the rule it exists to pin.
+
+This is the first schema-shaped divergence since
+[ADR 0010](0010-runtime-divergence-reconciliation.md) recorded that the two
+repositories' bundles were "in lockstep", and unlike ADR 0010's seven it **is**
+visible in a diff. That is what makes it cheap to settle.
+
+### Why now
+
+`git tag -l` is empty, `published.json` records `"releases": {}`, and the three
+`1.0.0` release pull requests are still open, so
+[ADR 0005](0005-platform-divergence-reconciliation.md) §1 applies. `check:compat`
+replays each release's documents out of that release's own tag; with no tags it
+has nothing to replay, and that emptiness is the mechanical form of the same
+fact.
+
+Withdrawing an enum member rejects documents that validate today. Under the
+window that costs maintainer approval and a breaking declaration in the commit
+trailer — not a `v2` directory and not a migration note. **Adding members stays
+free forever; withdrawing one is free now and never again.** That asymmetry, and
+not the platform's timetable, is why this is decided in this pass.
+
+## Decision
+
+### 1. `INTEGER` is withdrawn, and `NUMBER` replaces it
+
+`NUMBER` is JSON's own numeric kind and covers whole numbers and reals alike. An
+author who needs whole numbers writes the constraint rather than reaching for a
+second type: `type: NUMBER` with `pattern: '^-?[0-9]+$'`.
+
+**Rejected:** keeping both, which was the cheaper change — purely additive, and
+therefore not a breaking one. It was rejected on what it would have obliged the
+contract to answer. Is `5432.0` an `INTEGER`? Does an `INTEGER` output satisfy a
+`NUMBER` input, given that [blueprint §4.2](../../specifications/blueprint/v1/spec.md#connections)
+permits no widening in either direction? What would a bound mean on each? Three
+answers bought for a distinction the transport does not preserve — and under
+that no-widening rule, two near-synonymous numeric shapes make independently
+authored components reject each other over a difference no consumer can observe.
+
+What is given up is real and is worth naming: integrality is no longer
+expressible as a shape. The value schema has never carried `minimum` or
+`maximum`, so `pattern` was already the only quantitative lever the contract
+had, and `type: NUMBER` with a digit pattern says strictly more about a port
+than `type: INTEGER` ever did.
+
+### 2. `JSON` is added, and it names a string form rather than a host type
+
+A value reaches the workload as text. `default` is a string, `enum` is a list of
+strings, a `DECLARED` output's `value` is a string, and `pattern` is a regular
+expression over that same form. `type` therefore names the shape the value's
+**string form** takes, and a `JSON` value's string form is a JSON document.
+
+Read that way, `JSON` needs no change to any sibling field: a default is written
+`default: '{"logLevel":"info"}'`, quoted, and the
+[YAML profile](../../specifications/component/v1/spec.md#yaml-profile) makes the
+unquoted spelling self-diagnosing — it parses as a flow mapping and fails
+`type: string`.
+
+**Rejected:** widening `default` to accept arbitrary JSON. It would make one
+field mean two things depending on a sibling, and it would break the schema
+canonicalisation `ERR_CONFLICTING_INPUT_SCHEMA` compares blocks with.
+
+### 3. `pattern` and `enum` are forbidden on a `JSON` value
+
+One JSON value has many spellings. `{"a":1}` and `{ "a" : 1 }` are the same
+value, and so are `{"a":1,"b":2}` and `{"b":2,"a":1}`. Both keywords decide
+membership on the spelling, so a rule written with either would accept one
+author's formatter and reject another's.
+
+**Rejected:** permitting them under a defined JSON equality. Deciding whether
+equality is byte-wise, parsed, or key-order-sensitive is a new normative surface,
+bought for a rare case, and the failure it would leave behind is the one this
+contract refuses everywhere else — two implementations disagreeing about a valid
+document for a reason neither could see.
+
+### 4. `format` is confined to `STRING`
+
+Every `format` member — `EMAIL`, `URI`, `ENDPOINT_URL`, `CONNECTION_STRING` —
+names a lexical convention for text. The field's own `description` has always
+said so and nothing enforced it, so `format: EMAIL` on a `BOOLEAN` validates
+today and describes nothing.
+
+This is stated as a decision of its own rather than folded into §2, because it
+is the one narrowing here that `JSON` did not cause: it was reachable before
+this ADR and would have stayed reachable after it. It is taken now because the
+window closes once, and because a member table that lists which shapes accept
+`format` is the first place a reader will assume the restriction is real.
+
+**`semanticType` is deliberately not paired with `type`.** It tags the backing
+service a value addresses rather than the shape the value takes, so a `JSON`
+value describing a Postgres cluster is as legitimately `POSTGRES` as a
+connection string is. Restricting it would have foreclosed that.
+
+### 5. A `default` is not checked against the constraints beside it
+
+For any member. A `DECLARED` output may write `type: NUMBER` beside
+`value: 'banana'`; a `default` may ignore the `pattern` written next to it; a
+`JSON` default is not parsed.
+
+**Rejected:** a `semantic` rule requiring a `JSON` default to parse.
+`ERR_INVALID_VALUE` is registered `structural`, and the corpus gate refuses a
+`semantic` case declaring a `structural` code — so the rule would have cost a new diagnostic
+code, a registry row, and an implementation, and it would have made the newest
+member the only one whose `default` is validated. Also rejected:
+`contentMediaType`, for the reason
+[component §7.2](../../specifications/component/v1/spec.md#format-policy)
+already gives about annotations that assert nothing.
+
+The silence is recorded in §6.3 rather than closed. Closing it rejects documents
+that validate today, and this ADR spends the window on the vocabulary rather
+than on a check no member has ever had.
+
+### 6. The vocabulary is published in component §6.3, and blueprint cites it
+
+The `schema` block is shared by inputs and outputs, so it gets a subsection of
+its own rather than a home inside §6.1. Blueprint carries its own `$defs` and
+enforces the same rules, but does not restate the members:
+[ADR 0003](0003-controlled-vocabulary-placement.md) §2's rule against mirroring a
+published vocabulary applies to a sibling `spec.md` as much as to an external
+surface, and blueprint §7 already uses that construction for diagnostics.
+
+## Consequences
+
+**Documents are rejected that validate today**, in two ways. Any document using
+`type: INTEGER` — five conformance fixtures did, and they are migrated with this
+change. And any pairing `format` with a non-`STRING` type, which no fixture and
+no example carried, so §4 cost nothing to adopt and would have cost a major
+version to adopt later.
+
+**The window is spent further.** After the first tag, adding a fifth member is a
+minor release and withdrawing one is a `v2` directory.
+
+**Blueprint §4.2 gains a member that looks like a top type and is not.** A
+`STRING` output does not satisfy a `JSON` input, or the reverse. §4.2 now says so
+outright, because the first author to try it would otherwise read the rejection
+as a bug.
+
+**Nothing in `tools/` changes.** The runner compares `schema.type` by string
+equality and maps Ajv keywords to diagnostic codes generically, so the
+vocabulary was never encoded there. That is the property that let this change
+cost nine fixtures and no new code, no registry row, and no `UNCOVERED` entry.
+
+**The vocabulary stops being defined in an informative field.** Three
+requirement IDs — `COMP-VAL-001`, `COMP-VAL-002`, `COMP-VAL-003` — now name
+rules a document can violate, and each is pinned by a fixture.
+
+## Follow-ups
+
+1. Confirm `musher-dev/platform` adopts §3 and §4. Its PR #1997 carries the
+ vocabulary but not the `pattern`, `enum` and `format` restrictions, so this is
+ the one half of the change where this repository is ahead rather than behind.
+2. `minimum` and `maximum` on a value schema are a separate design surface —
+ whether a bound reads the string or the parsed number, how it composes with
+ `pattern`, what it means on a `STRING`. Additive, so it stays available after
+ the window closes, which is why it is not decided here.
diff --git a/docs/traceability.md b/docs/traceability.md
index c282018..3772973 100644
--- a/docs/traceability.md
+++ b/docs/traceability.md
@@ -34,6 +34,9 @@ this table makes visible is which rules the corpus actually holds down.
| [`COMP-EP-002`](../specifications/component/v1/spec.md#COMP-EP-002) | [§Health probes](../specifications/component/v1/spec.md#health) | [`component/v1/semantic/002-probe-names-unknown-endpoint`](../conformance/component/v1/semantic/002-probe-names-unknown-endpoint/)
[`component/v1/semantic/003-platform-default-names-unknown-endpoint`](../conformance/component/v1/semantic/003-platform-default-names-unknown-endpoint/) |
| [`COMP-EP-003`](../specifications/component/v1/spec.md#COMP-EP-003) | [§Health probes](../specifications/component/v1/spec.md#health) | [`component/v1/semantic/008-probe-on-tcp-endpoint`](../conformance/component/v1/semantic/008-probe-on-tcp-endpoint/)
[`component/v1/semantic/009-platform-default-on-tcp-endpoint`](../conformance/component/v1/semantic/009-platform-default-on-tcp-endpoint/) |
| [`COMP-EP-004`](../specifications/component/v1/spec.md#COMP-EP-004) | [§Inputs](../specifications/component/v1/spec.md#inputs) | [`component/v1/semantic/004-platform-default-names-private-endpoint`](../conformance/component/v1/semantic/004-platform-default-names-private-endpoint/) |
+| [`COMP-VAL-001`](../specifications/component/v1/spec.md#COMP-VAL-001) | [§Value schemas](../specifications/component/v1/spec.md#value-schema) | [`blueprint/v1/structural/024-integer-parameter-type`](../conformance/blueprint/v1/structural/024-integer-parameter-type/)
[`blueprint/v1/structural/026-json-parameter`](../conformance/blueprint/v1/structural/026-json-parameter/)
[`component/v1/structural/039-integer-value-type`](../conformance/component/v1/structural/039-integer-value-type/)
[`component/v1/structural/040-number-value-type`](../conformance/component/v1/structural/040-number-value-type/)
[`component/v1/structural/041-json-value-type`](../conformance/component/v1/structural/041-json-value-type/) |
+| [`COMP-VAL-002`](../specifications/component/v1/spec.md#COMP-VAL-002) | [§Value schemas](../specifications/component/v1/spec.md#value-schema) | [`blueprint/v1/structural/025-json-parameter-with-pattern`](../conformance/blueprint/v1/structural/025-json-parameter-with-pattern/)
[`component/v1/structural/042-json-value-with-pattern`](../conformance/component/v1/structural/042-json-value-with-pattern/)
[`component/v1/structural/043-json-value-with-enum`](../conformance/component/v1/structural/043-json-value-with-enum/) |
+| [`COMP-VAL-003`](../specifications/component/v1/spec.md#COMP-VAL-003) | [§Value schemas](../specifications/component/v1/spec.md#value-schema) | [`component/v1/structural/044-format-on-a-non-string-value`](../conformance/component/v1/structural/044-format-on-a-non-string-value/) |
| [`COMP-YAML-001`](../specifications/component/v1/spec.md#COMP-YAML-001) | [§The Musher YAML profile](../specifications/component/v1/spec.md#yaml-profile) | [`blueprint/v1/parser/004-malformed-yaml`](../conformance/blueprint/v1/parser/004-malformed-yaml/)
[`component/v1/parser/004-malformed-yaml`](../conformance/component/v1/parser/004-malformed-yaml/)
[`listing/v1/parser/004-malformed-yaml`](../conformance/listing/v1/parser/004-malformed-yaml/) |
| [`COMP-YAML-002`](../specifications/component/v1/spec.md#COMP-YAML-002) | [§The Musher YAML profile](../specifications/component/v1/spec.md#yaml-profile) | [`component/v1/parser/012-byte-order-mark`](../conformance/component/v1/parser/012-byte-order-mark/) |
| [`COMP-YAML-003`](../specifications/component/v1/spec.md#COMP-YAML-003) | [§The Musher YAML profile](../specifications/component/v1/spec.md#yaml-profile) | [`component/v1/parser/013-crlf-line-endings`](../conformance/component/v1/parser/013-crlf-line-endings/) |
@@ -61,4 +64,4 @@ this table makes visible is which rules the corpus actually holds down.
| [`LIST-MEDIA-003`](../specifications/listing/v1/spec.md#LIST-MEDIA-003) | [§Media](../specifications/listing/v1/spec.md#media) | [`listing/v1/semantic/001-duplicate-screenshot-basename`](../conformance/listing/v1/semantic/001-duplicate-screenshot-basename/) |
| [`LIST-MEDIA-004`](../specifications/listing/v1/spec.md#LIST-MEDIA-004) | [§Resolution after ingest](../specifications/listing/v1/spec.md#media-resolution) | — |
-33 of 37 requirement(s) are pinned by at least one conformance case.
+36 of 40 requirement(s) are pinned by at least one conformance case.
diff --git a/specifications/blueprint/v1/schemas/dist/blueprint.schema.json b/specifications/blueprint/v1/schemas/dist/blueprint.schema.json
index 7402cca..51935f1 100644
--- a/specifications/blueprint/v1/schemas/dist/blueprint.schema.json
+++ b/specifications/blueprint/v1/schemas/dist/blueprint.schema.json
@@ -403,6 +403,53 @@
"BlueprintParameterValueSchema": {
"description": "Data contract of an install-form parameter value (JSON-Schema subset).",
"additionalProperties": false,
+ "allOf": [
+ {
+ "$comment": "spec.md §5.3, the block component §6.3 defines. A JSON value's string form is a JSON document, and one value has many spellings — insignificant whitespace, member order, number formatting. `pattern` and `enum` both decide membership on the spelling, so neither can state a rule about the value.",
+ "if": {
+ "properties": {
+ "type": {
+ "const": "JSON"
+ }
+ },
+ "required": [
+ "type"
+ ]
+ },
+ "then": {
+ "properties": {
+ "enum": {
+ "maxItems": 0
+ },
+ "pattern": {
+ "type": "null"
+ }
+ }
+ }
+ },
+ {
+ "$comment": "spec.md §5.3, the block component §6.3 defines. Every `format` member names a lexical convention for text, so the field only refines a STRING. `type` is REQUIRED, so this is a `not` inside the `if` rather than an `else`: a document omitting `type` already reports ERR_MISSING_FIELD and should not also collect a format diagnostic.",
+ "if": {
+ "properties": {
+ "type": {
+ "not": {
+ "const": "STRING"
+ }
+ }
+ },
+ "required": [
+ "type"
+ ]
+ },
+ "then": {
+ "properties": {
+ "format": {
+ "type": "null"
+ }
+ }
+ }
+ }
+ ],
"properties": {
"default": {
"description": "Pre-filled value, or null when the deploying user must supply one.",
@@ -417,14 +464,14 @@
"default": null
},
"enum": {
- "description": "Allowed values when the value is a closed enumeration; empty when unconstrained.",
+ "description": "Allowed values when the value is a closed enumeration; empty when unconstrained. MUST be empty where the type is JSON.",
"items": {
"type": "string"
},
"type": "array"
},
"format": {
- "description": "Semantic refinement of a STRING value: EMAIL, URI, ENDPOINT_URL, or CONNECTION_STRING; null when unrefined.",
+ "description": "Semantic refinement of a STRING value: EMAIL, URI, ENDPOINT_URL, or CONNECTION_STRING; null when unrefined. MUST be null where the type is not STRING.",
"anyOf": [
{
"enum": [
@@ -447,7 +494,7 @@
"type": "boolean"
},
"pattern": {
- "description": "Regular expression the supplied value must match, or null when unconstrained.",
+ "description": "Regular expression the supplied value must match, or null when unconstrained. MUST be null where the type is JSON.",
"anyOf": [
{
"type": "string"
@@ -459,11 +506,12 @@
"default": null
},
"type": {
- "description": "Primitive shape of the value: STRING, INTEGER, or BOOLEAN.",
+ "description": "Shape of the value's string form: STRING is any text, NUMBER a JSON number, BOOLEAN true or false, JSON a JSON document of any kind.",
"enum": [
"STRING",
- "INTEGER",
- "BOOLEAN"
+ "NUMBER",
+ "BOOLEAN",
+ "JSON"
],
"type": "string"
}
diff --git a/specifications/blueprint/v1/schemas/src/blueprint.schema.json b/specifications/blueprint/v1/schemas/src/blueprint.schema.json
index a3c0911..ad04095 100644
--- a/specifications/blueprint/v1/schemas/src/blueprint.schema.json
+++ b/specifications/blueprint/v1/schemas/src/blueprint.schema.json
@@ -402,6 +402,53 @@
},
"BlueprintParameterValueSchema": {
"additionalProperties": false,
+ "allOf": [
+ {
+ "$comment": "spec.md §5.3, the block component §6.3 defines. A JSON value's string form is a JSON document, and one value has many spellings — insignificant whitespace, member order, number formatting. `pattern` and `enum` both decide membership on the spelling, so neither can state a rule about the value.",
+ "if": {
+ "properties": {
+ "type": {
+ "const": "JSON"
+ }
+ },
+ "required": [
+ "type"
+ ]
+ },
+ "then": {
+ "properties": {
+ "enum": {
+ "maxItems": 0
+ },
+ "pattern": {
+ "type": "null"
+ }
+ }
+ }
+ },
+ {
+ "$comment": "spec.md §5.3, the block component §6.3 defines. Every `format` member names a lexical convention for text, so the field only refines a STRING. `type` is REQUIRED, so this is a `not` inside the `if` rather than an `else`: a document omitting `type` already reports ERR_MISSING_FIELD and should not also collect a format diagnostic.",
+ "if": {
+ "properties": {
+ "type": {
+ "not": {
+ "const": "STRING"
+ }
+ }
+ },
+ "required": [
+ "type"
+ ]
+ },
+ "then": {
+ "properties": {
+ "format": {
+ "type": "null"
+ }
+ }
+ }
+ }
+ ],
"description": "Data contract of an install-form parameter value (JSON-Schema subset).",
"properties": {
"default": {
@@ -417,7 +464,7 @@
"description": "Pre-filled value, or null when the deploying user must supply one."
},
"enum": {
- "description": "Allowed values when the value is a closed enumeration; empty when unconstrained.",
+ "description": "Allowed values when the value is a closed enumeration; empty when unconstrained. MUST be empty where the type is JSON.",
"items": {
"type": "string"
},
@@ -439,7 +486,7 @@
}
],
"default": null,
- "description": "Semantic refinement of a STRING value: EMAIL, URI, ENDPOINT_URL, or CONNECTION_STRING; null when unrefined."
+ "description": "Semantic refinement of a STRING value: EMAIL, URI, ENDPOINT_URL, or CONNECTION_STRING; null when unrefined. MUST be null where the type is not STRING."
},
"isSensitive": {
"default": false,
@@ -456,14 +503,15 @@
}
],
"default": null,
- "description": "Regular expression the supplied value must match, or null when unconstrained."
+ "description": "Regular expression the supplied value must match, or null when unconstrained. MUST be null where the type is JSON."
},
"type": {
- "description": "Primitive shape of the value: STRING, INTEGER, or BOOLEAN.",
+ "description": "Shape of the value's string form: STRING is any text, NUMBER a JSON number, BOOLEAN true or false, JSON a JSON document of any kind.",
"enum": [
"STRING",
- "INTEGER",
- "BOOLEAN"
+ "NUMBER",
+ "BOOLEAN",
+ "JSON"
],
"type": "string"
}
diff --git a/specifications/blueprint/v1/spec.md b/specifications/blueprint/v1/spec.md
index 7ebaafa..d279430 100644
--- a/specifications/blueprint/v1/spec.md
+++ b/specifications/blueprint/v1/spec.md
@@ -277,7 +277,7 @@ the value, with nothing saying which arrives. That silence is a gap rather than
a considered permission; closing it rejects compositions that validate today.
**The two ends MUST fit.** Resolving both ends establishes only that they
-exist. A `STRING` output wired into an `INTEGER` input satisfies every rule
+exist. A `STRING` output wired into a `NUMBER` input satisfies every rule
above, and fails at deploy time inside the consuming workload — the failure
shape [§5.2](#merge) rejected for input merging, on the grounds that it lands
"a long way from the two documents that disagreed and with nothing pointing
@@ -291,12 +291,22 @@ are `semantic`, both need the referenced component documents, and both anchor
at the connection's `fromOutput`.
**`type` MUST be equal.** A mismatch is `ERR_INCOMPATIBLE_TYPE`. No widening is
-permitted, in either direction. An `INTEGER` output feeding a `STRING` input
+permitted, in either direction. A `NUMBER` output feeding a `STRING` input
looks harmless — everything is a string by the time it reaches a container —
but *which* string is a decision each language's formatter makes differently,
-and a contract that permitted the wire would be promising a value it cannot
-describe. An author who wants the conversion writes an output that already has
-the type the consumer asked for.
+and `5432`, `5432.0` and `5.432e3` are one value with three spellings. A
+contract that permitted the wire would be promising a value it cannot describe.
+An author who wants the conversion writes an output that already has the type
+the consumer asked for.
+
+**`JSON` is not an exception, and it is the member most likely to look like
+one.** It names a value whose string form is a JSON document
+([component §6.3](../../component/v1/spec.md#value-schema)), not a supertype of
+the other three. A `STRING` output does not satisfy a `JSON` input, and a
+`JSON` output does not satisfy a `STRING` one — in either direction and for
+the same reason: a consumer that will parse what it receives and one that will
+not are asking for different things, and the wire is the last place that
+difference is visible.
**`semanticType` MUST agree where the consumer names one.** A consumer
declaring `null` accepts any producer: it has said the value is not specific to
@@ -612,7 +622,7 @@ equal that input's, and a mismatch is `ERR_INCOMPATIBLE_PARAMETER_TYPE`,
anchored at `/spec/parameters//schema/type`. The deploying user is
validated against the parameter's schema and the component then receives the
result against its own: a `STRING` accepted at the form where the workload
-expects an `INTEGER` is the failure [§4.2](#connections) rejected for
+expects a `NUMBER` is the failure [§4.2](#connections) rejected for
connections and [§5.2](#merge) rejected for merging, arriving through a third
door.
@@ -628,6 +638,17 @@ already. Without the same rule here, moving a generated secret onto the override
path is enough to lose it — and `isSensitive` defaults to `false`, so losing it
takes no more than not mentioning it.
+**A parameter's `schema` is the block
+[component §6.3](../../component/v1/spec.md#value-schema) defines**, minus
+`semanticType`. The `type` vocabulary, the `JSON` restrictions on `pattern`
+and `enum`, and the `STRING` restriction on `format` are that section's and
+are not restated here; this family's schema enforces them on the same terms and
+in the same phase. Naming where a vocabulary is published rather than mirroring
+it is the rule
+[ADR 0003](../../../docs/adr/0003-controlled-vocabulary-placement.md) §2 sets
+out, and a sibling `spec.md` is no more exempt from it than an external
+surface.
+
**What v1 does not compare.** `format`, `enum`, `pattern`, `default`,
`isSensitive` and `ui` take no part in whether a parameter covers an input. A
parameter whose `pattern` admits more than the input's does is accepted, and so
diff --git a/specifications/component/v1/schemas/dist/component.schema.json b/specifications/component/v1/schemas/dist/component.schema.json
index fa1e0cd..689735b 100644
--- a/specifications/component/v1/schemas/dist/component.schema.json
+++ b/specifications/component/v1/schemas/dist/component.schema.json
@@ -770,6 +770,53 @@
"ComponentValueSchema": {
"description": "Data contract of an input or output value (JSON-Schema subset).",
"additionalProperties": false,
+ "allOf": [
+ {
+ "$comment": "spec.md §6.3. A JSON value's string form is a JSON document, and one value has many spellings — insignificant whitespace, member order, number formatting. `pattern` and `enum` both decide membership on the spelling, so neither can state a rule about the value.",
+ "if": {
+ "properties": {
+ "type": {
+ "const": "JSON"
+ }
+ },
+ "required": [
+ "type"
+ ]
+ },
+ "then": {
+ "properties": {
+ "enum": {
+ "maxItems": 0
+ },
+ "pattern": {
+ "type": "null"
+ }
+ }
+ }
+ },
+ {
+ "$comment": "spec.md §6.3. Every `format` member names a lexical convention for text, so the field only refines a STRING. `type` is REQUIRED, so this is a `not` inside the `if` rather than an `else`: a document omitting `type` already reports ERR_MISSING_FIELD and should not also collect a format diagnostic.",
+ "if": {
+ "properties": {
+ "type": {
+ "not": {
+ "const": "STRING"
+ }
+ }
+ },
+ "required": [
+ "type"
+ ]
+ },
+ "then": {
+ "properties": {
+ "format": {
+ "type": "null"
+ }
+ }
+ }
+ }
+ ],
"properties": {
"default": {
"description": "Pre-filled value when none is supplied, or null when the value must be provided.",
@@ -784,14 +831,14 @@
"default": null
},
"enum": {
- "description": "Allowed values when the value is a closed enumeration; empty when unconstrained.",
+ "description": "Allowed values when the value is a closed enumeration; empty when unconstrained. MUST be empty where the type is JSON.",
"items": {
"type": "string"
},
"type": "array"
},
"format": {
- "description": "Semantic refinement of a STRING value: EMAIL, URI, ENDPOINT_URL, or CONNECTION_STRING; null when unrefined.",
+ "description": "Semantic refinement of a STRING value: EMAIL, URI, ENDPOINT_URL, or CONNECTION_STRING; null when unrefined. MUST be null where the type is not STRING.",
"anyOf": [
{
"enum": [
@@ -814,7 +861,7 @@
"type": "boolean"
},
"pattern": {
- "description": "Regular expression the value must match, or null when unconstrained.",
+ "description": "Regular expression the value must match, or null when unconstrained. MUST be null where the type is JSON.",
"anyOf": [
{
"type": "string"
@@ -847,11 +894,12 @@
"default": null
},
"type": {
- "description": "Primitive shape of the value: STRING, INTEGER, or BOOLEAN.",
+ "description": "Shape of the value's string form: STRING is any text, NUMBER a JSON number, BOOLEAN true or false, JSON a JSON document of any kind.",
"enum": [
"STRING",
- "INTEGER",
- "BOOLEAN"
+ "NUMBER",
+ "BOOLEAN",
+ "JSON"
],
"type": "string"
}
diff --git a/specifications/component/v1/schemas/src/component.schema.json b/specifications/component/v1/schemas/src/component.schema.json
index 7a71c5b..8cc23fd 100644
--- a/specifications/component/v1/schemas/src/component.schema.json
+++ b/specifications/component/v1/schemas/src/component.schema.json
@@ -769,6 +769,53 @@
},
"ComponentValueSchema": {
"additionalProperties": false,
+ "allOf": [
+ {
+ "$comment": "spec.md §6.3. A JSON value's string form is a JSON document, and one value has many spellings — insignificant whitespace, member order, number formatting. `pattern` and `enum` both decide membership on the spelling, so neither can state a rule about the value.",
+ "if": {
+ "properties": {
+ "type": {
+ "const": "JSON"
+ }
+ },
+ "required": [
+ "type"
+ ]
+ },
+ "then": {
+ "properties": {
+ "enum": {
+ "maxItems": 0
+ },
+ "pattern": {
+ "type": "null"
+ }
+ }
+ }
+ },
+ {
+ "$comment": "spec.md §6.3. Every `format` member names a lexical convention for text, so the field only refines a STRING. `type` is REQUIRED, so this is a `not` inside the `if` rather than an `else`: a document omitting `type` already reports ERR_MISSING_FIELD and should not also collect a format diagnostic.",
+ "if": {
+ "properties": {
+ "type": {
+ "not": {
+ "const": "STRING"
+ }
+ }
+ },
+ "required": [
+ "type"
+ ]
+ },
+ "then": {
+ "properties": {
+ "format": {
+ "type": "null"
+ }
+ }
+ }
+ }
+ ],
"description": "Data contract of an input or output value (JSON-Schema subset).",
"properties": {
"default": {
@@ -784,7 +831,7 @@
"description": "Pre-filled value when none is supplied, or null when the value must be provided."
},
"enum": {
- "description": "Allowed values when the value is a closed enumeration; empty when unconstrained.",
+ "description": "Allowed values when the value is a closed enumeration; empty when unconstrained. MUST be empty where the type is JSON.",
"items": {
"type": "string"
},
@@ -806,7 +853,7 @@
}
],
"default": null,
- "description": "Semantic refinement of a STRING value: EMAIL, URI, ENDPOINT_URL, or CONNECTION_STRING; null when unrefined."
+ "description": "Semantic refinement of a STRING value: EMAIL, URI, ENDPOINT_URL, or CONNECTION_STRING; null when unrefined. MUST be null where the type is not STRING."
},
"isSensitive": {
"default": false,
@@ -823,7 +870,7 @@
}
],
"default": null,
- "description": "Regular expression the value must match, or null when unconstrained."
+ "description": "Regular expression the value must match, or null when unconstrained. MUST be null where the type is JSON."
},
"semanticType": {
"anyOf": [
@@ -847,11 +894,12 @@
"description": "Backing-service compatibility tag the Blueprint publish gate matches when wiring this value into a consumer input; null when the value is not specific to a backing service."
},
"type": {
- "description": "Primitive shape of the value: STRING, INTEGER, or BOOLEAN.",
+ "description": "Shape of the value's string form: STRING is any text, NUMBER a JSON number, BOOLEAN true or false, JSON a JSON document of any kind.",
"enum": [
"STRING",
- "INTEGER",
- "BOOLEAN"
+ "NUMBER",
+ "BOOLEAN",
+ "JSON"
],
"type": "string"
}
diff --git a/specifications/component/v1/spec.md b/specifications/component/v1/spec.md
index 601ca7b..20127a6 100644
--- a/specifications/component/v1/spec.md
+++ b/specifications/component/v1/spec.md
@@ -653,6 +653,73 @@ leaves nothing unresolved.
carries the two diagnostics, because the connection is what joins the two ends
and a component document sees only one of them.
+### 6.3 Value schemas
+
+An input and an output carry the same `schema` block, and this section defines
+it once. `type` is its only REQUIRED property.
+
+**A value is carried as text.** Whatever an input receives or an output
+publishes reaches the workload as an environment variable, so `type` does not
+name a host language's type — it names the shape the value's **string form**
+takes. That is why `default` is a string, why `enum` is a list of strings, why a
+`DECLARED` output's `value` is a string, and why `pattern` is a regular
+expression over the same form.
+
+**`COMP-VAL-001`** — `type` MUST be one of four
+members. Anything else is rejected in the `structural` phase with
+`ERR_INVALID_VALUE`.
+
+| `type` | The string form is | `format` | `pattern` | `enum` |
+|---|---|---|---|---|
+| `STRING` | any text | permitted | permitted | permitted |
+| `NUMBER` | a JSON number — `5432`, `-1`, `2.5` | MUST be null | permitted | permitted |
+| `BOOLEAN` | `true` or `false`, in lower case | MUST be null | permitted | permitted |
+| `JSON` | a JSON value of any kind — object, array, string, number, boolean or null | MUST be null | MUST be null | MUST be empty |
+
+**There is no integer member, and the omission is deliberate.** `NUMBER` is
+JSON's own numeric kind and covers whole numbers and reals alike. An author who
+needs whole numbers writes the constraint rather than reaching for a second
+type: `type: NUMBER` with `pattern: '^-?[0-9]+$'`. A separate `INTEGER` would
+have to answer whether `5432.0` is one, whether an integer output satisfies a
+number input under [blueprint §4.2](../../blueprint/v1/spec.md#connections)'s
+no-widening rule, and what a bound would mean on each — three answers bought for
+a distinction the transport does not preserve.
+
+**`COMP-VAL-002`** — Where `type` is `JSON`, `pattern`
+MUST be null and `enum` MUST be empty. Both are `structural`: a non-null
+`pattern` is `ERR_INVALID_TYPE` and a non-empty `enum` is `ERR_INVALID_VALUE`.
+
+One JSON value has many spellings. `{"a":1}` and `{ "a" : 1 }` are the same
+value, and so are `{"a":1,"b":2}` and `{"b":2,"a":1}`. A regular expression and
+a string enumeration each decide membership on the spelling, so a rule written
+with either would accept one author's formatter and reject another's. Permitting
+them and leaving "equal" undefined is the worse option: two implementations
+would then disagree about a valid document for a reason neither could see.
+
+**`COMP-VAL-003`** — Where `type` is not `STRING`,
+`format` MUST be null. `structural`, `ERR_INVALID_TYPE`.
+
+Every `format` member — `EMAIL`, `URI`, `ENDPOINT_URL`, `CONNECTION_STRING` —
+names a lexical convention for text. `format: EMAIL` on a `BOOLEAN` describes
+nothing, and that it validates today is an accident of the two fields never
+having been described together. This clause is about the Musher field `format`,
+not the JSON Schema keyword; [§7.2](#format-policy) is that.
+
+**`semanticType` is not paired with `type`.** It tags the backing service a
+value addresses rather than the shape the value takes, so nothing here restricts
+which types may carry which tag — a `JSON` value describing a Postgres cluster
+is as legitimately `POSTGRES` as a connection string is. That the pairing goes
+unchecked in either direction is a gap rather than a considered permission.
+
+**What v1 does not check.** No phase tests a **value** against the shape its
+`schema` declares. A `DECLARED` output may write `type: NUMBER` beside
+`value: 'banana'`; a `default` may ignore the `pattern` and the `enum` written
+beside it; a `JSON` value's `default` is not parsed. None of this is new with
+`JSON` — it has been true of every member since v1 — and it is recorded here
+rather than described aspirationally, because the table above is the first place
+a reader could reasonably expect the check to be. Closing any of them rejects
+documents that validate today.
+
## 7. Validation layers
Structural validation is one layer of four. An implementation MUST apply them