Summary
Blueprint §5.3 (added in #36) makes ERR_UNBOUND_PARAMETER a negative existence claim — "a key matching no USER input of any node" — that an offline implementation cannot substantiate when the graph contains a published (UUID) component reference, and the reference implementation currently false-positives in exactly that case.
The mechanism
In tools/src/semantic.ts, checkGraphAgainstItem resolves only repo-local references into resolved (a published reference "belongs to the capability phase"), and checkParameterBinding then builds its declared input-key map from resolved alone:
const component = resolved.get(node)
// A published reference resolves in the capability phase, so its inputs are
// unreadable here. §5.3: an implementation MUST NOT report an input it was
// never given the means to read.
if (component === undefined) continue
The unbound loop that follows has no such guard: any parameter key absent from declared is reported at /spec/parameters/<key> as "names no USER input of any node" — including a parameter whose only covered input lives on the node the validator just declined to read.
Why this is a defect rather than the documented behaviour
§5.3 closes with:
A blueprint mixing the two forms is checked against the repo-local half and no further, and an implementation MUST NOT report an input it was never given the means to read.
That principle is honoured for ERR_UNCOVERED_REQUIRED_INPUT (an unreadable node's inputs never enter the coverage set) and for ERR_INCOMPATIBLE_PARAMETER_TYPE (nothing to compare). But ERR_UNBOUND_PARAMETER is the same principle's mirror image on the parameter side: the diagnostic asserts a fact about every node's inputs while the implementation was given the means to read only some of them. A valid blueprint —
spec:
components:
db:
component: ./components/postgres.yaml # local
size: general.standard.small
connections: {}
api:
component: 018f6b3a-… # published; declares USER input `apiKey`
size: general.standard.small
connections: {}
parameters:
apiKey: # binds the published node's input — and only that
schema: {type: STRING, isSensitive: true}
isRequired: true
ui: {label: API key}
— is rejected in the semantic phase with ERR_UNBOUND_PARAMETER at /spec/parameters/apiKey, even though the parameter is legitimately bound. The document then cannot be made valid offline at all except by removing the parameter, which under §5.3's second rule may in turn be the wrong move (the published component's required input still needs a guaranteed value at deploy).
Proposed fix
Suppress ERR_UNBOUND_PARAMETER when the graph contains at least one reference the implementation could not read (a published reference, or a local reference already carrying its own ERR_COMPONENT_NOT_FOUND). The claim becomes decidable again only when every node's inputs were readable. Options, in order of preference:
- Wholesale suppression — one sentence in §5.3 ("where any node's component is unreadable, an implementation MUST NOT report
ERR_UNBOUND_PARAMETER"), one if in checkParameterBinding. Symmetric with how the other two rules already degrade.
- Re-classify the unbound check as
capability for mixed graphs — heavier, and splits one rule across two phases.
Every existing corpus fixture tree is all-local, so option 1 changes no current case; a new fixture pinning the mixed-form pass case would close the gap.
A minor adjacent observation (fold in or ignore)
§4.4 presents cpuDedication: shared and dedicated as symmetric pins. On the platform, placement maps only "dedicated" to a host capability (placement/domain/compute_constraints.py — strict=True over {"dedicated": CPU_DEDICATED}), so shared narrows nothing today. Either wording that makes shared explicitly "the default made explicit", or a platform-side must-be-shared capability, would remove an asymmetry a reader cannot see.
Found while reviewing #35/#36 for platform alignment (musher-dev/platform is bumping its conformance pin to 311be77 and implementing the new rules; its implementation will apply the option-1 guard in the meantime).
Summary
Blueprint §5.3 (added in #36) makes
ERR_UNBOUND_PARAMETERa negative existence claim — "a key matching noUSERinput of any node" — that an offline implementation cannot substantiate when the graph contains a published (UUID) component reference, and the reference implementation currently false-positives in exactly that case.The mechanism
In
tools/src/semantic.ts,checkGraphAgainstItemresolves only repo-local references intoresolved(a published reference "belongs to the capability phase"), andcheckParameterBindingthen builds itsdeclaredinput-key map fromresolvedalone:The unbound loop that follows has no such guard: any parameter key absent from
declaredis reported at/spec/parameters/<key>as "names no USER input of any node" — including a parameter whose only covered input lives on the node the validator just declined to read.Why this is a defect rather than the documented behaviour
§5.3 closes with:
That principle is honoured for
ERR_UNCOVERED_REQUIRED_INPUT(an unreadable node's inputs never enter the coverage set) and forERR_INCOMPATIBLE_PARAMETER_TYPE(nothing to compare). ButERR_UNBOUND_PARAMETERis the same principle's mirror image on the parameter side: the diagnostic asserts a fact about every node's inputs while the implementation was given the means to read only some of them. A valid blueprint —— is rejected in the
semanticphase withERR_UNBOUND_PARAMETERat/spec/parameters/apiKey, even though the parameter is legitimately bound. The document then cannot be made valid offline at all except by removing the parameter, which under §5.3's second rule may in turn be the wrong move (the published component's required input still needs a guaranteed value at deploy).Proposed fix
Suppress
ERR_UNBOUND_PARAMETERwhen the graph contains at least one reference the implementation could not read (a published reference, or a local reference already carrying its ownERR_COMPONENT_NOT_FOUND). The claim becomes decidable again only when every node's inputs were readable. Options, in order of preference:ERR_UNBOUND_PARAMETER"), oneifincheckParameterBinding. Symmetric with how the other two rules already degrade.capabilityfor mixed graphs — heavier, and splits one rule across two phases.Every existing corpus fixture tree is all-local, so option 1 changes no current case; a new fixture pinning the mixed-form pass case would close the gap.
A minor adjacent observation (fold in or ignore)
§4.4 presents
cpuDedication: sharedanddedicatedas symmetric pins. On the platform, placement maps only"dedicated"to a host capability (placement/domain/compute_constraints.py—strict=Trueover{"dedicated": CPU_DEDICATED}), sosharednarrows nothing today. Either wording that makessharedexplicitly "the default made explicit", or a platform-side must-be-shared capability, would remove an asymmetry a reader cannot see.Found while reviewing #35/#36 for platform alignment (musher-dev/platform is bumping its conformance pin to
311be77and implementing the new rules; its implementation will apply the option-1 guard in the meantime).