Summary
The three spec.md files carry ~20 TODO markers. Most of them have answers already
implemented and gated in musher-dev/platform — this issue supplies them, with the
enforcing call site for each, so the prose can be written from the behaviour rather than
invented alongside it.
One of them is not a gap but a contradiction, and it should be settled before the rest.
⚠️ blueprint/v1/spec.md §4.1 mandates acyclic graphs; the platform deliberately permits cycles
The spec says:
Cycle detection. The connection graph MUST be acyclic. This is the canonical example
of a rule JSON Schema cannot express; it belongs to the semantic phase with code
ERR_DEPENDENCY_CYCLE.
The platform has no cycle detection, and its absence is a design decision recorded in
ADR 0116 (blueprint-connection-materialization) and asserted at four independent call sites.
From component/domain/model.py::_assert_template_namespaces:
An output may depend on the producing node's own resolved parameters and its own
addressing — never on a value the node received over an inbound connection. That invariant
is what lets the compose loop resolve every node's outputs in a single pass with no
topological ordering, which in turn keeps a cyclic graph (mutual service discovery)
legal.
The mechanism: because an output is a function of the producing node alone, every node's
outputs are resolvable before any node is materialised. Compose runs two passes — resolve
all outputs, then bind all inbound edges — and needs no topological order. A cycle is not a
resolution hazard, so it is not an error. The intended use case named in the code is mutual
service discovery: two services that each need the other's address.
The rule that is enforced is narrower and different: an output value template may reference
only params.* and self.*. That is what makes cycles safe, and it is the invariant worth
writing into §4.1 — it constrains something JSON Schema genuinely cannot express, and it
carries a real diagnostic.
Decision needed. Either the spec adopts the platform's position and §4.1 becomes the
params.*/self.* output-namespace rule with ERR_DEPENDENCY_CYCLE withdrawn, or the spec
holds its position and the platform gains a cycle check that would ban a case it currently
supports. I would not implement ERR_DEPENDENCY_CYCLE on the strength of a TODO — hence
raising it rather than filling it in.
component/v1/spec.md
§5.1 Source — image-ref pinning
Implemented in component/domain/image_ref.py::validate_pinned_image_ref; enforced on the
corpus by tests/architecture/test_catalog_manifests.py::test_image_refs_are_pinned.
- Accepted: a digest pin matching
@sha256:[0-9a-f]{64}$, or a tag pin whose tag is not
floating.
- Rejected: no tag at all (implicit
:latest), and this exact floating set —
latest, main, main-stable, master, stable, edge, nightly, dev, rolling
(compared case-insensitively).
- Rationale in the module docstring: floating tags "mutate under the curator and silently
shift deployments."
Suggested code: ERR_UNPINNED_IMAGE, structural if expressed as a pattern, semantic
if the floating-tag set stays a list.
§5 Workload — kind × field validity
spec.md currently says the schema "permits both" and that this "MUST be resolved before v1
is declared stable." It is already resolved in
seeds/catalog/manifest.py::assert_component_shape:
| Rule |
Kinds |
| ≥1 endpoint required |
SERVICE |
| endpoints forbidden |
WORKER, JOB, CRON |
runCommand required |
JOB, CRON |
cronExpression required |
CRON |
health.readiness required |
SERVICE with ≥1 PUBLIC endpoint |
source.imageRef required, build forbidden |
source.type: IMAGE |
source.gitRepo and build both required |
source.type: GIT |
| input keys unique within a component |
all |
So: endpoints on a WORKER is an error, not ignored; schedule is required on CRON
rather than merely permitted. All of these are semantic-phase — each is a cross-field rule
JSON Schema 2020-12 could express with if/then but which reads far better as prose plus
a fixture.
§5.4 Health probes
health.readiness is required exactly when the component is a SERVICE with at least one
PUBLIC endpoint — a private-only service is exempt. Probe defaults live on
ComponentProbeRequest; path is its only required property.
§5.5 Volumes
sizeGib and mountPath are required; accessMode and isReadOnly are optional. The
platform does not currently enforce mount-path absoluteness, non-overlap between two volumes,
or size bounds — those are genuine gaps in both projects rather than undocumented behaviour,
and worth stating as such rather than describing aspirationally.
§6 Configuration contract
- A generated input (one carrying
generator) MUST have suppliedBy: USER and
schema.isSensitive: true. Enforced by
test_generated_inputs_are_user_supplied_and_sensitive; the rationale is that a
platform-minted value is secret material and rides on a USER input (ADR 0046).
- Output value templates may reference only the
params.* and self.* namespaces —
self.privateAddress.<endpoint>, self.publicUrl. Referencing an inbound connection value
is rejected. This is the invariant discussed above.
blueprint/v1/spec.md
§5 Parameters — derivation and merge
Both TODOs have concrete answers.
Derivation. When spec.parameters is empty, the effective parameter set is derived from
the graph's merged USER-supplied inputs
(seeds/catalog/importer.py::_derived_parameters). An input with no ui block is skipped —
it never appears on the install form. Generator-carrying inputs are included, so a client
can show the auto-generated count.
Merge rule. component/domain/services.py::merge_user_inputs — key-first-wins in
canonical node order, implemented as setdefault over nodes sorted by
(ordering, componentId). A key declared by an earlier node shadows the same key on a later
node, and the later declaration's schema is discarded rather than merged or rejected.
That last part deserves scrutiny before it is written down as normative. Two components
declaring adminPassword with different schemas silently resolve to the first node's
schema. First-wins is a defensible rule, but it is currently an implementation detail rather
than a stated contract, and "silently discards a conflicting declaration" is the kind of
behaviour a specification should either bless explicitly or replace with a
ERR_CONFLICTING_INPUT_SCHEMA diagnostic.
§3 Identity, and listing/v1/spec.md §3
Enforced by seeds/catalog/loader.py::_assert_item_identity:
- the item directory name equals both documents'
metadata.slug;
listing.metadata.version == blueprint.metadata.version;
- every blueprint node's component reference resolves to a file in the same item directory,
and every such file is referenced — an unreferenced component file is an error.
That last rule is not in the spec at all and is worth adding: it is what stops a directory
accumulating dead component documents.
listing/v1/spec.md
§5 Media
Enforced by seeds/catalog/manifest.py::_assert_media_path plus a load-time existence check:
- the path MUST be relative and MUST NOT contain
..;
- its first segment MUST be
media;
- its extension MUST be one of
.png, .jpg, .jpeg, .webp (case-insensitive);
- the target MUST exist on disk;
- two screenshots MUST NOT share a basename.
ERR_PATH_ESCAPE and ERR_MEDIA_NOT_FOUND in §7 both map cleanly. The "first segment must
be media" rule is stronger than §5's current wording ("relative to the listing document")
and should be stated, since it is what actually gets enforced.
§4 Presentation
No answer to supply. The platform stores description as Markdown and renders it in the
storefront, but neither project constrains the permitted subset — so the §4 concern about
rendering untrusted Markdown as an injection surface is a real open item, not documented
behaviour. Worth keeping the TODO and tracking it as a security question rather than closing
it from the implementation.
Conformance fixtures this implies
conformance/README.md notes cases exist for parser and structural only, and that
semantic cases land "once the corresponding rules are written into spec.md". Nearly
everything above is semantic. Once the clauses land, the fixture set follows almost
mechanically — every rule in the tables above is one positive and one negative case.
I am happy to contribute the fixtures alongside whichever clauses you accept.
Related
specifications/{component,blueprint,listing}/v1/spec.md — the TODO markers
conformance/README.md → "Coverage status"
- platform ADR 0116 (blueprint-connection-materialization), ADR 0046 (generated inputs)
Summary
The three
spec.mdfiles carry ~20TODOmarkers. Most of them have answers alreadyimplemented and gated in
musher-dev/platform— this issue supplies them, with theenforcing call site for each, so the prose can be written from the behaviour rather than
invented alongside it.
One of them is not a gap but a contradiction, and it should be settled before the rest.
blueprint/v1/spec.md§4.1 mandates acyclic graphs; the platform deliberately permits cyclesThe spec says:
The platform has no cycle detection, and its absence is a design decision recorded in
ADR 0116 (blueprint-connection-materialization) and asserted at four independent call sites.
From
component/domain/model.py::_assert_template_namespaces:The mechanism: because an output is a function of the producing node alone, every node's
outputs are resolvable before any node is materialised. Compose runs two passes — resolve
all outputs, then bind all inbound edges — and needs no topological order. A cycle is not a
resolution hazard, so it is not an error. The intended use case named in the code is mutual
service discovery: two services that each need the other's address.
The rule that is enforced is narrower and different: an output value template may reference
only
params.*andself.*. That is what makes cycles safe, and it is the invariant worthwriting into §4.1 — it constrains something JSON Schema genuinely cannot express, and it
carries a real diagnostic.
Decision needed. Either the spec adopts the platform's position and §4.1 becomes the
params.*/self.*output-namespace rule withERR_DEPENDENCY_CYCLEwithdrawn, or the specholds its position and the platform gains a cycle check that would ban a case it currently
supports. I would not implement
ERR_DEPENDENCY_CYCLEon the strength of a TODO — henceraising it rather than filling it in.
component/v1/spec.md§5.1 Source — image-ref pinning
Implemented in
component/domain/image_ref.py::validate_pinned_image_ref; enforced on thecorpus by
tests/architecture/test_catalog_manifests.py::test_image_refs_are_pinned.@sha256:[0-9a-f]{64}$, or a tag pin whose tag is notfloating.
:latest), and this exact floating set —latest,main,main-stable,master,stable,edge,nightly,dev,rolling(compared case-insensitively).
shift deployments."
Suggested code:
ERR_UNPINNED_IMAGE,structuralif expressed as apattern,semanticif the floating-tag set stays a list.
§5 Workload — kind × field validity
spec.mdcurrently says the schema "permits both" and that this "MUST be resolved before v1is declared stable." It is already resolved in
seeds/catalog/manifest.py::assert_component_shape:SERVICEWORKER,JOB,CRONrunCommandrequiredJOB,CRONcronExpressionrequiredCRONhealth.readinessrequiredSERVICEwith ≥1PUBLICendpointsource.imageRefrequired,buildforbiddensource.type: IMAGEsource.gitRepoandbuildboth requiredsource.type: GITSo:
endpointson aWORKERis an error, not ignored;scheduleis required onCRONrather than merely permitted. All of these are
semantic-phase — each is a cross-field ruleJSON Schema 2020-12 could express with
if/thenbut which reads far better as prose plusa fixture.
§5.4 Health probes
health.readinessis required exactly when the component is aSERVICEwith at least onePUBLICendpoint — a private-only service is exempt. Probe defaults live onComponentProbeRequest;pathis its only required property.§5.5 Volumes
sizeGibandmountPathare required;accessModeandisReadOnlyare optional. Theplatform does not currently enforce mount-path absoluteness, non-overlap between two volumes,
or size bounds — those are genuine gaps in both projects rather than undocumented behaviour,
and worth stating as such rather than describing aspirationally.
§6 Configuration contract
generator) MUST havesuppliedBy: USERandschema.isSensitive: true. Enforced bytest_generated_inputs_are_user_supplied_and_sensitive; the rationale is that aplatform-minted value is secret material and rides on a USER input (ADR 0046).
params.*andself.*namespaces —self.privateAddress.<endpoint>,self.publicUrl. Referencing an inbound connection valueis rejected. This is the invariant discussed above.
blueprint/v1/spec.md§5 Parameters — derivation and merge
Both TODOs have concrete answers.
Derivation. When
spec.parametersis empty, the effective parameter set is derived fromthe graph's merged
USER-supplied inputs(
seeds/catalog/importer.py::_derived_parameters). An input with nouiblock is skipped —it never appears on the install form. Generator-carrying inputs are included, so a client
can show the auto-generated count.
Merge rule.
component/domain/services.py::merge_user_inputs— key-first-wins incanonical node order, implemented as
setdefaultover nodes sorted by(ordering, componentId). A key declared by an earlier node shadows the same key on a laternode, and the later declaration's schema is discarded rather than merged or rejected.
That last part deserves scrutiny before it is written down as normative. Two components
declaring
adminPasswordwith different schemas silently resolve to the first node'sschema. First-wins is a defensible rule, but it is currently an implementation detail rather
than a stated contract, and "silently discards a conflicting declaration" is the kind of
behaviour a specification should either bless explicitly or replace with a
ERR_CONFLICTING_INPUT_SCHEMAdiagnostic.§3 Identity, and
listing/v1/spec.md§3Enforced by
seeds/catalog/loader.py::_assert_item_identity:metadata.slug;listing.metadata.version == blueprint.metadata.version;and every such file is referenced — an unreferenced component file is an error.
That last rule is not in the spec at all and is worth adding: it is what stops a directory
accumulating dead component documents.
listing/v1/spec.md§5 Media
Enforced by
seeds/catalog/manifest.py::_assert_media_pathplus a load-time existence check:..;media;.png,.jpg,.jpeg,.webp(case-insensitive);ERR_PATH_ESCAPEandERR_MEDIA_NOT_FOUNDin §7 both map cleanly. The "first segment mustbe
media" rule is stronger than §5's current wording ("relative to the listing document")and should be stated, since it is what actually gets enforced.
§4 Presentation
No answer to supply. The platform stores
descriptionas Markdown and renders it in thestorefront, but neither project constrains the permitted subset — so the §4 concern about
rendering untrusted Markdown as an injection surface is a real open item, not documented
behaviour. Worth keeping the TODO and tracking it as a security question rather than closing
it from the implementation.
Conformance fixtures this implies
conformance/README.mdnotes cases exist forparserandstructuralonly, and thatsemanticcases land "once the corresponding rules are written intospec.md". Nearlyeverything above is
semantic. Once the clauses land, the fixture set follows almostmechanically — every rule in the tables above is one positive and one negative case.
I am happy to contribute the fixtures alongside whichever clauses you accept.
Related
specifications/{component,blueprint,listing}/v1/spec.md— the TODO markersconformance/README.md→ "Coverage status"