Skip to content

Fill the spec.md TODOs from implemented platform behaviour — and settle the cycle-detection contradiction #9

Description

@justinmerrell

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_inputskey-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)

Metadata

Metadata

Assignees

No one assigned

    Labels

    documentationImprovements or additions to documentation

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions