diff --git a/README.md b/README.md index 7b90c35..8c1522f 100644 --- a/README.md +++ b/README.md @@ -36,7 +36,10 @@ Or bind by glob in VS Code `settings.json`: ```json { "yaml.schemas": { - "https://schemas.musher.dev/component/v1/component.schema.json": "**/components/*.yaml", + "https://schemas.musher.dev/component/v1/component.schema.json": [ + "**/components/*.yaml", + "**/component-*.yaml" + ], "https://schemas.musher.dev/blueprint/v1/blueprint.schema.json": "**/blueprint.yaml", "https://schemas.musher.dev/listing/v1/listing.schema.json": "**/listing.yaml" } diff --git a/catalog.json b/catalog.json index 3d02d4e..1221171 100644 --- a/catalog.json +++ b/catalog.json @@ -17,7 +17,11 @@ "description": "Musher component document — one reusable workload definition.", "fileMatch": [ "**/components/*.yaml", - "**/components/*.yml" + "**/components/*.yml", + "**/component.yaml", + "**/component.yml", + "**/component-*.yaml", + "**/component-*.yml" ], "name": "Musher Component Document", "url": "https://schemas.musher.dev/component/v1/component.schema.json", diff --git a/conformance/blueprint/v1/cases.json b/conformance/blueprint/v1/cases.json index 0613a80..30fb155 100644 --- a/conformance/blueprint/v1/cases.json +++ b/conformance/blueprint/v1/cases.json @@ -31,6 +31,36 @@ "id": "structural-005-nested-unknown-field", "phase": "structural", "path": "structural/005-nested-unknown-field" + }, + { + "id": "structural-006-local-component-reference", + "phase": "structural", + "path": "structural/006-local-component-reference" + }, + { + "id": "structural-007-published-component-reference", + "phase": "structural", + "path": "structural/007-published-component-reference" + }, + { + "id": "structural-008-bare-component-stem", + "phase": "structural", + "path": "structural/008-bare-component-stem" + }, + { + "id": "structural-009-version-pinned-on-local-reference", + "phase": "structural", + "path": "structural/009-version-pinned-on-local-reference" + }, + { + "id": "structural-010-published-reference-without-version", + "phase": "structural", + "path": "structural/010-published-reference-without-version" + }, + { + "id": "structural-011-traversal-inside-local-reference", + "phase": "structural", + "path": "structural/011-traversal-inside-local-reference" } ] } diff --git a/conformance/blueprint/v1/structural/001-minimal-valid/case.yaml b/conformance/blueprint/v1/structural/001-minimal-valid/case.yaml index da6dfac..80e39fd 100644 --- a/conformance/blueprint/v1/structural/001-minimal-valid/case.yaml +++ b/conformance/blueprint/v1/structural/001-minimal-valid/case.yaml @@ -6,7 +6,7 @@ metadata: spec: components: db: - component: postgres + component: ./components/postgres.yaml size: general.standard.small connections: {} parameters: {} diff --git a/conformance/blueprint/v1/structural/002-wrong-kind/case.yaml b/conformance/blueprint/v1/structural/002-wrong-kind/case.yaml index 3671d5e..cb2e52b 100644 --- a/conformance/blueprint/v1/structural/002-wrong-kind/case.yaml +++ b/conformance/blueprint/v1/structural/002-wrong-kind/case.yaml @@ -8,7 +8,7 @@ metadata: spec: components: db: - component: postgres + component: ./components/postgres.yaml size: general.standard.small connections: {} parameters: {} diff --git a/conformance/blueprint/v1/structural/003-unknown-field/case.yaml b/conformance/blueprint/v1/structural/003-unknown-field/case.yaml index 81b0060..9d57ad3 100644 --- a/conformance/blueprint/v1/structural/003-unknown-field/case.yaml +++ b/conformance/blueprint/v1/structural/003-unknown-field/case.yaml @@ -8,7 +8,7 @@ metadata: spec: components: db: - component: postgres + component: ./components/postgres.yaml size: general.standard.small connections: {} parameters: {} diff --git a/conformance/blueprint/v1/structural/004-unsupported-spec-version/case.yaml b/conformance/blueprint/v1/structural/004-unsupported-spec-version/case.yaml index 23e0a68..5784b8a 100644 --- a/conformance/blueprint/v1/structural/004-unsupported-spec-version/case.yaml +++ b/conformance/blueprint/v1/structural/004-unsupported-spec-version/case.yaml @@ -8,7 +8,7 @@ metadata: spec: components: db: - component: postgres + component: ./components/postgres.yaml size: general.standard.small connections: {} parameters: {} diff --git a/conformance/blueprint/v1/structural/005-nested-unknown-field/case.yaml b/conformance/blueprint/v1/structural/005-nested-unknown-field/case.yaml index e0ab8c2..39308c9 100644 --- a/conformance/blueprint/v1/structural/005-nested-unknown-field/case.yaml +++ b/conformance/blueprint/v1/structural/005-nested-unknown-field/case.yaml @@ -9,7 +9,7 @@ metadata: spec: components: db: - component: postgres + component: ./components/postgres.yaml size: general.standard.small connections: {} parameters: diff --git a/conformance/blueprint/v1/structural/006-local-component-reference/case.yaml b/conformance/blueprint/v1/structural/006-local-component-reference/case.yaml new file mode 100644 index 0000000..7e872e4 --- /dev/null +++ b/conformance/blueprint/v1/structural/006-local-component-reference/case.yaml @@ -0,0 +1,21 @@ +# The repo-local reference form, in both layouts it has to support: a nested +# `components/` directory and a flat sibling file. The `./` prefix is what +# selects the local resolver — without it a bare slug and a UUID are +# indistinguishable, since a UUID beginning with a hex letter satisfies the +# slug grammar. +specVersion: v1 +kind: BLUEPRINT +metadata: + slug: web-and-database + version: 1 +spec: + components: + db: + component: ./components/postgres.yaml + size: general.standard.small + connections: {} + web: + component: ./component-web.yaml + size: general.standard.small + connections: {} + parameters: {} diff --git a/conformance/blueprint/v1/structural/006-local-component-reference/metadata.json b/conformance/blueprint/v1/structural/006-local-component-reference/metadata.json new file mode 100644 index 0000000..07367fd --- /dev/null +++ b/conformance/blueprint/v1/structural/006-local-component-reference/metadata.json @@ -0,0 +1,7 @@ +{ + "id": "structural-006-local-component-reference", + "phase": "structural", + "expected": "pass", + "clause": "specifications/blueprint/v1/spec.md#component-reference", + "summary": "A repo-local component reference validates from any directory layout, nested or flat." +} diff --git a/conformance/blueprint/v1/structural/007-published-component-reference/case.yaml b/conformance/blueprint/v1/structural/007-published-component-reference/case.yaml new file mode 100644 index 0000000..c6e754f --- /dev/null +++ b/conformance/blueprint/v1/structural/007-published-component-reference/case.yaml @@ -0,0 +1,16 @@ +# The published reference form: a component UUID plus the lineage version it +# pins. The version is required here because, unlike a local reference, there +# is no document on disk whose `metadata.version` could supply it. +specVersion: v1 +kind: BLUEPRINT +metadata: + slug: postgres + version: 1 +spec: + components: + db: + component: 550e8400-e29b-41d4-a716-446655440000 + componentVersion: 3 + size: general.standard.small + connections: {} + parameters: {} diff --git a/conformance/blueprint/v1/structural/007-published-component-reference/metadata.json b/conformance/blueprint/v1/structural/007-published-component-reference/metadata.json new file mode 100644 index 0000000..423c34f --- /dev/null +++ b/conformance/blueprint/v1/structural/007-published-component-reference/metadata.json @@ -0,0 +1,7 @@ +{ + "id": "structural-007-published-component-reference", + "phase": "structural", + "expected": "pass", + "clause": "specifications/blueprint/v1/spec.md#component-reference", + "summary": "A published component reference is a UUID carrying the lineage version it pins." +} diff --git a/conformance/blueprint/v1/structural/008-bare-component-stem/case.yaml b/conformance/blueprint/v1/structural/008-bare-component-stem/case.yaml new file mode 100644 index 0000000..cb04d36 --- /dev/null +++ b/conformance/blueprint/v1/structural/008-bare-component-stem/case.yaml @@ -0,0 +1,16 @@ +# A bare file stem, the form this family used before the reference was +# discriminated by shape. It is rejected because it is ambiguous: the slug +# grammar `^[a-z][a-z0-9-]{0,61}[a-z0-9]$` also matches a UUID that begins +# with a hex letter, so a reader cannot tell a stem from a published id. +specVersion: v1 +kind: BLUEPRINT +metadata: + slug: postgres + version: 1 +spec: + components: + db: + component: postgres + size: general.standard.small + connections: {} + parameters: {} diff --git a/conformance/blueprint/v1/structural/008-bare-component-stem/diagnostics.json b/conformance/blueprint/v1/structural/008-bare-component-stem/diagnostics.json new file mode 100644 index 0000000..1f82cf9 --- /dev/null +++ b/conformance/blueprint/v1/structural/008-bare-component-stem/diagnostics.json @@ -0,0 +1,6 @@ +[ + { + "code": "ERR_INVALID_VALUE", + "path": "/spec/components/db/component" + } +] diff --git a/conformance/blueprint/v1/structural/008-bare-component-stem/metadata.json b/conformance/blueprint/v1/structural/008-bare-component-stem/metadata.json new file mode 100644 index 0000000..dfdd25e --- /dev/null +++ b/conformance/blueprint/v1/structural/008-bare-component-stem/metadata.json @@ -0,0 +1,7 @@ +{ + "id": "structural-008-bare-component-stem", + "phase": "structural", + "expected": "fail", + "clause": "specifications/blueprint/v1/spec.md#component-reference", + "summary": "A reference carrying neither a relative-path prefix nor a UUID matches no resolver and is rejected." +} diff --git a/conformance/blueprint/v1/structural/009-version-pinned-on-local-reference/case.yaml b/conformance/blueprint/v1/structural/009-version-pinned-on-local-reference/case.yaml new file mode 100644 index 0000000..3609111 --- /dev/null +++ b/conformance/blueprint/v1/structural/009-version-pinned-on-local-reference/case.yaml @@ -0,0 +1,16 @@ +# A local reference that also pins a version. The referenced document already +# carries its own `metadata.version`, so a second version here could disagree +# with it and there would be no rule saying which one wins. +specVersion: v1 +kind: BLUEPRINT +metadata: + slug: postgres + version: 1 +spec: + components: + db: + component: ./components/postgres.yaml + componentVersion: 3 + size: general.standard.small + connections: {} + parameters: {} diff --git a/conformance/blueprint/v1/structural/009-version-pinned-on-local-reference/diagnostics.json b/conformance/blueprint/v1/structural/009-version-pinned-on-local-reference/diagnostics.json new file mode 100644 index 0000000..7f9dbed --- /dev/null +++ b/conformance/blueprint/v1/structural/009-version-pinned-on-local-reference/diagnostics.json @@ -0,0 +1,6 @@ +[ + { + "code": "ERR_INVALID_VALUE", + "path": "/spec/components/db/componentVersion" + } +] diff --git a/conformance/blueprint/v1/structural/009-version-pinned-on-local-reference/metadata.json b/conformance/blueprint/v1/structural/009-version-pinned-on-local-reference/metadata.json new file mode 100644 index 0000000..3b168bf --- /dev/null +++ b/conformance/blueprint/v1/structural/009-version-pinned-on-local-reference/metadata.json @@ -0,0 +1,7 @@ +{ + "id": "structural-009-version-pinned-on-local-reference", + "phase": "structural", + "expected": "fail", + "clause": "specifications/blueprint/v1/spec.md#component-reference", + "summary": "componentVersion on a local reference is rejected; the version is the referenced document's own." +} diff --git a/conformance/blueprint/v1/structural/010-published-reference-without-version/case.yaml b/conformance/blueprint/v1/structural/010-published-reference-without-version/case.yaml new file mode 100644 index 0000000..38755b4 --- /dev/null +++ b/conformance/blueprint/v1/structural/010-published-reference-without-version/case.yaml @@ -0,0 +1,15 @@ +# A published reference with no version. Nothing on disk carries the lineage +# version for this form, so omitting it would leave the node pinned to +# whatever the registry serves at deploy time. +specVersion: v1 +kind: BLUEPRINT +metadata: + slug: postgres + version: 1 +spec: + components: + db: + component: 550e8400-e29b-41d4-a716-446655440000 + size: general.standard.small + connections: {} + parameters: {} diff --git a/conformance/blueprint/v1/structural/010-published-reference-without-version/diagnostics.json b/conformance/blueprint/v1/structural/010-published-reference-without-version/diagnostics.json new file mode 100644 index 0000000..ec8128f --- /dev/null +++ b/conformance/blueprint/v1/structural/010-published-reference-without-version/diagnostics.json @@ -0,0 +1,6 @@ +[ + { + "code": "ERR_MISSING_FIELD", + "path": "/spec/components/db" + } +] diff --git a/conformance/blueprint/v1/structural/010-published-reference-without-version/metadata.json b/conformance/blueprint/v1/structural/010-published-reference-without-version/metadata.json new file mode 100644 index 0000000..dd0c7a7 --- /dev/null +++ b/conformance/blueprint/v1/structural/010-published-reference-without-version/metadata.json @@ -0,0 +1,7 @@ +{ + "id": "structural-010-published-reference-without-version", + "phase": "structural", + "expected": "fail", + "clause": "specifications/blueprint/v1/spec.md#component-reference", + "summary": "A published reference without componentVersion is rejected rather than floating on the registry." +} diff --git a/conformance/blueprint/v1/structural/011-traversal-inside-local-reference/case.yaml b/conformance/blueprint/v1/structural/011-traversal-inside-local-reference/case.yaml new file mode 100644 index 0000000..722251e --- /dev/null +++ b/conformance/blueprint/v1/structural/011-traversal-inside-local-reference/case.yaml @@ -0,0 +1,17 @@ +# A local reference spelling a traversal into an interior segment. Every +# segment must begin with a letter or digit, so `..` cannot appear except in +# the leading prefix. Two spellings of one target would otherwise both be +# valid, and the semantic containment check would have to normalise before it +# could compare. +specVersion: v1 +kind: BLUEPRINT +metadata: + slug: postgres + version: 1 +spec: + components: + db: + component: ./components/../../etc/passwd.yaml + size: general.standard.small + connections: {} + parameters: {} diff --git a/conformance/blueprint/v1/structural/011-traversal-inside-local-reference/diagnostics.json b/conformance/blueprint/v1/structural/011-traversal-inside-local-reference/diagnostics.json new file mode 100644 index 0000000..1f82cf9 --- /dev/null +++ b/conformance/blueprint/v1/structural/011-traversal-inside-local-reference/diagnostics.json @@ -0,0 +1,6 @@ +[ + { + "code": "ERR_INVALID_VALUE", + "path": "/spec/components/db/component" + } +] diff --git a/conformance/blueprint/v1/structural/011-traversal-inside-local-reference/metadata.json b/conformance/blueprint/v1/structural/011-traversal-inside-local-reference/metadata.json new file mode 100644 index 0000000..2baca40 --- /dev/null +++ b/conformance/blueprint/v1/structural/011-traversal-inside-local-reference/metadata.json @@ -0,0 +1,7 @@ +{ + "id": "structural-011-traversal-inside-local-reference", + "phase": "structural", + "expected": "fail", + "clause": "specifications/blueprint/v1/spec.md#component-reference", + "summary": "A local reference with a traversal in an interior segment is rejected before containment is considered." +} diff --git a/specifications/blueprint/v1/examples/minimal.yaml b/specifications/blueprint/v1/examples/minimal.yaml index 26e1964..9e908c4 100644 --- a/specifications/blueprint/v1/examples/minimal.yaml +++ b/specifications/blueprint/v1/examples/minimal.yaml @@ -2,6 +2,11 @@ # # A single-component blueprint. Parameters are empty, so they are derived from # the merged USER-supplied inputs of the referenced components. +# +# `component` is a repo-local reference: it resolves relative to the directory +# holding this file, and the version it deploys is that document's own +# `metadata.version`. The paths here are illustrative — examples are validated +# structurally, and reference resolution belongs to the semantic phase. specVersion: v1 kind: BLUEPRINT metadata: @@ -10,7 +15,7 @@ metadata: spec: components: db: - component: postgres + component: ./components/postgres.yaml size: general.standard.small connections: {} parameters: {} diff --git a/specifications/blueprint/v1/examples/published-reference.yaml b/specifications/blueprint/v1/examples/published-reference.yaml new file mode 100644 index 0000000..fec8893 --- /dev/null +++ b/specifications/blueprint/v1/examples/published-reference.yaml @@ -0,0 +1,22 @@ +# yaml-language-server: $schema=https://schemas.musher.dev/blueprint/v1/blueprint.schema.json +# +# The published reference form. `component` is the UUID of a component the +# platform already holds, and `componentVersion` pins the lineage version to +# deploy — required here, because no document on disk carries it. +# +# The same field carries both forms; the shape of the value selects the +# resolver. A local reference would begin `./` or `../`, and must not carry a +# `componentVersion`. +specVersion: v1 +kind: BLUEPRINT +metadata: + slug: postgres + version: 1 +spec: + components: + db: + component: 550e8400-e29b-41d4-a716-446655440000 + componentVersion: 3 + size: general.standard.small + connections: {} + parameters: {} diff --git a/specifications/blueprint/v1/examples/web-and-database.yaml b/specifications/blueprint/v1/examples/web-and-database.yaml index 328df9d..6fd0e67 100644 --- a/specifications/blueprint/v1/examples/web-and-database.yaml +++ b/specifications/blueprint/v1/examples/web-and-database.yaml @@ -3,6 +3,9 @@ # Two components wired together. Connections are consumer-anchored: the `web` # node declares what it consumes and from which role, so a component never # needs to know who will depend on it. +# +# The two nodes show that a repo-local reference does not mandate a directory +# layout — `db` names a document under `components/`, `web` a flat sibling. specVersion: v1 kind: BLUEPRINT metadata: @@ -11,11 +14,11 @@ metadata: spec: components: db: - component: postgres + component: ./components/postgres.yaml size: general.standard.small connections: {} web: - component: web-server + component: ./component-web-server.yaml size: general.standard.small connections: DATABASE_URL: diff --git a/specifications/blueprint/v1/schemas/dist/blueprint.schema.json b/specifications/blueprint/v1/schemas/dist/blueprint.schema.json index 2a37193..8d6d4f2 100644 --- a/specifications/blueprint/v1/schemas/dist/blueprint.schema.json +++ b/specifications/blueprint/v1/schemas/dist/blueprint.schema.json @@ -182,8 +182,23 @@ "type": "object" }, "BlueprintNode": { - "description": "One node of the blueprint's graph, keyed by node name.\n\nA node places one component document — named by ``component``, the stem of\nthe sibling ``components/.yaml`` file — onto a compute ``size``, with\noptional host constraints (``advanced``) and inbound wires\n(``connections``).", + "description": "One node of the blueprint's graph, keyed by node name.\n\nA node places one component document — named by ``component`` — onto a\ncompute ``size``, with optional host constraints (``advanced``) and inbound\nwires (``connections``).\n\nThe form of ``component`` selects how it resolves: a reference beginning\n``./`` or ``../`` names a document in the same repository, and one shaped\nlike a UUID names a published component and additionally requires\n``componentVersion``. See spec.md §4.1.", "additionalProperties": false, + "else": { + "required": [ + "componentVersion" + ] + }, + "if": { + "properties": { + "component": { + "pattern": "^\\.{1,2}/" + } + }, + "required": [ + "component" + ] + }, "properties": { "advanced": { "description": "Optional advanced compute pins that narrow the eligible-host set at schedule time.", @@ -198,11 +213,24 @@ "default": null }, "component": { - "description": "Stem of the sibling components/.yaml file this node deploys. The pinned version comes from that file's metadata.version.", - "maxLength": 128, - "minLength": 1, + "description": "Reference to the component document this node deploys, in one of two forms. A repo-local reference begins ./ or ../ and ends .yaml or .yml, and resolves relative to the directory holding this blueprint document; its version is the referenced document's metadata.version. A published reference is the component's UUID and requires componentVersion. The relative-path prefix is what tells the two apart — a bare slug is not distinguishable from a UUID.", + "$comment": "Both patterns are lookahead-free so they compile under RE2 as well as ECMA-262. A path segment must begin with a letter or digit, which is how `.` and `..` are excluded as interior segments without a negative lookahead; that leaves one spelling per target and keeps the containment check in the semantic phase from having to normalise first.", + "anyOf": [ + { + "pattern": "^(?:\\./|(?:\\.\\./)+)(?:[A-Za-z0-9][A-Za-z0-9._-]*/)*[A-Za-z0-9][A-Za-z0-9._-]*\\.ya?ml$" + }, + { + "pattern": "^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$" + } + ], + "maxLength": 256, "type": "string" }, + "componentVersion": { + "description": "Lineage version pinned by a published reference. Required on a published reference and rejected on a repo-local one, where the referenced document's own metadata.version supplies it.", + "minimum": 1, + "type": "integer" + }, "connections": { "description": "Inbound wires for this node, keyed by the consumer input key each fills (value names the producer role and output; one wire per input).", "additionalProperties": { @@ -221,6 +249,11 @@ "component", "size" ], + "then": { + "properties": { + "componentVersion": false + } + }, "type": "object" }, "BlueprintParameter": { diff --git a/specifications/blueprint/v1/schemas/src/blueprint.schema.json b/specifications/blueprint/v1/schemas/src/blueprint.schema.json index 521711e..6890181 100644 --- a/specifications/blueprint/v1/schemas/src/blueprint.schema.json +++ b/specifications/blueprint/v1/schemas/src/blueprint.schema.json @@ -183,7 +183,27 @@ }, "BlueprintNode": { "additionalProperties": false, - "description": "One node of the blueprint's graph, keyed by node name.\n\nA node places one component document — named by ``component``, the stem of\nthe sibling ``components/.yaml`` file — onto a compute ``size``, with\noptional host constraints (``advanced``) and inbound wires\n(``connections``).", + "description": "One node of the blueprint's graph, keyed by node name.\n\nA node places one component document — named by ``component`` — onto a\ncompute ``size``, with optional host constraints (``advanced``) and inbound\nwires (``connections``).\n\nThe form of ``component`` selects how it resolves: a reference beginning\n``./`` or ``../`` names a document in the same repository, and one shaped\nlike a UUID names a published component and additionally requires\n``componentVersion``. See spec.md §4.1.", + "else": { + "required": [ + "componentVersion" + ] + }, + "if": { + "properties": { + "component": { + "pattern": "^\\.{1,2}/" + } + }, + "required": [ + "component" + ] + }, + "then": { + "properties": { + "componentVersion": false + } + }, "properties": { "advanced": { "anyOf": [ @@ -198,11 +218,24 @@ "description": "Optional advanced compute pins that narrow the eligible-host set at schedule time." }, "component": { - "description": "Stem of the sibling components/.yaml file this node deploys. The pinned version comes from that file's metadata.version.", - "maxLength": 128, - "minLength": 1, + "anyOf": [ + { + "pattern": "^(?:\\./|(?:\\.\\./)+)(?:[A-Za-z0-9][A-Za-z0-9._-]*/)*[A-Za-z0-9][A-Za-z0-9._-]*\\.ya?ml$" + }, + { + "pattern": "^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$" + } + ], + "$comment": "Both patterns are lookahead-free so they compile under RE2 as well as ECMA-262. A path segment must begin with a letter or digit, which is how `.` and `..` are excluded as interior segments without a negative lookahead; that leaves one spelling per target and keeps the containment check in the semantic phase from having to normalise first.", + "description": "Reference to the component document this node deploys, in one of two forms. A repo-local reference begins ./ or ../ and ends .yaml or .yml, and resolves relative to the directory holding this blueprint document; its version is the referenced document's metadata.version. A published reference is the component's UUID and requires componentVersion. The relative-path prefix is what tells the two apart — a bare slug is not distinguishable from a UUID.", + "maxLength": 256, "type": "string" }, + "componentVersion": { + "description": "Lineage version pinned by a published reference. Required on a published reference and rejected on a repo-local one, where the referenced document's own metadata.version supplies it.", + "minimum": 1, + "type": "integer" + }, "connections": { "additionalProperties": { "$ref": "#/$defs/BlueprintConnection" diff --git a/specifications/blueprint/v1/spec.md b/specifications/blueprint/v1/spec.md index 586cb35..448161e 100644 --- a/specifications/blueprint/v1/spec.md +++ b/specifications/blueprint/v1/spec.md @@ -22,6 +22,19 @@ are wired to one another. The blueprint is the unit of deployment. +**Out of scope for this document** + +- One workload's own definition → `component` family +- Storefront presentation → `listing` family +- API request and response bodies, including the optimistic-lock token carried + by a declarative-apply call → the Musher API. A document a human writes into + a repository does not carry a lock token; see [§3](#identity). +- The `apiVersion: musher.dev/v1`, `kind: App` shape read by `musher deploy`. + [ADR 0001](../../../docs/adr/0001-canonical-repository-architecture.md) §2 + declines to ratify it. It is superseded by a repo-local `blueprint` document + paired with its `component` documents, which the reference form in + [§4.1](#component-reference) makes expressible. + ## 2. Document envelope ```yaml @@ -36,6 +49,12 @@ spec: { components: {…}, parameters: {…} } ## 3. Identity +`metadata` carries `slug` and `version`, and nothing else. A record identifier +and a concurrency token — the `id` and `rowVersion` an API carries on a +declarative apply — describe a row in a control plane, not a document. They +MUST NOT appear on a blueprint document, and a validator MUST reject them with +`ERR_UNKNOWN_FIELD` like any other unknown property. + > **TODO** — `metadata.slug` MUST equal the containing item directory name; > `metadata.version` MUST agree with the sibling listing document. State the > diagnostic codes for each violation and which phase detects them. @@ -48,16 +67,85 @@ this blueprint and carries no meaning outside it. > **TODO** — Node name grammar and uniqueness. -> **TODO** — Resolution of the `component` field. In the catalog authoring -> dialect it names a sibling file stem under `components/`; on the API it is a -> component id plus version. State both bindings explicitly and which contexts -> use which — this is the single largest source of confusion in the current -> format. - > **TODO** — `size` MUST name a Compute Profile in `family.tier.size` form. > State whether an unknown profile is a `semantic` or a `capability` failure. -### 4.1 Connections +### 4.1 Component reference + +A node names the component it deploys with a single field, `component`. The +**form** of the value selects how it resolves. There is exactly one field for +this concept; a second parallel key naming the same slot is what made the +earlier dialects mutually unreadable. + +Two forms are defined. + +**Repo-local.** The reference MUST begin with `./` or `../` and MUST end with +`.yaml` or `.yml`. It is resolved relative to the directory containing the +referencing blueprint document. Every path segment MUST begin with a letter or +a digit, so `.` and `..` are not valid interior segments: each referenced +document has exactly one spelling. + +```yaml +db: + component: ./components/postgres.yaml + size: general.standard.small + connections: {} +``` + +`componentVersion` MUST NOT be present. The version deployed is the referenced +document's `metadata.version`, so a second version here could contradict it +with no rule saying which wins. + +**Published.** The reference is the component's UUID, and `componentVersion` +MUST be present. + +```yaml +db: + component: 550e8400-e29b-41d4-a716-446655440000 + componentVersion: 3 + size: general.standard.small + connections: {} +``` + +A reference matching neither form MUST be rejected in the `structural` phase. + +**Why the prefix is required.** A bare name is not distinguishable from a +UUID: the slug grammar `^[a-z][a-z0-9-]{0,61}[a-z0-9]$` matches +`e29b8400-e29b-41d4-a716-446655440000`. Without a prefix no validator could +decide which resolver a reference wanted, so the prefix is load-bearing rather +than decorative. + +**The local form imposes no directory layout.** `./components/postgres.yaml` +and `./component-web.yaml` are equally valid. A blueprint MAY sit beside its +components in one flat directory. + +**Where each form resolves.** A repo-local reference is resolved in the +`semantic` phase: it reads the filesystem, which +[component §7](../../component/v1/spec.md#validation-layers) permits, since +that phase MUST NOT require network access. A published reference cannot be +resolved under that constraint and therefore belongs to the `capability` +phase, server-side. An offline client MUST NOT report a published reference as +unresolvable; it has not been given the means to check. A published reference +naming no component, or naming one without the requested `componentVersion`, +MUST be rejected with `ERR_UNKNOWN_COMPONENT`. + +A context holding no filesystem location for the document — a document +submitted over an API, for example — has no base directory to resolve against. +It MUST reject a repo-local reference with `ERR_COMPONENT_NOT_FOUND` rather +than assume a base. Guessing one is what let the same field mean two different +things depending on who was reading it. + +A repo-local reference MUST resolve to a document inside the item root. +`../` segments that escape it MUST be rejected with `ERR_REFERENCE_ESCAPE`, and +a reference naming no document with `ERR_COMPONENT_NOT_FOUND`. Both are +`semantic`, both are decided after normalising the path — containment is a +property of the resolved location, not of the spelling. + +**Reserved.** A third form, `/`, is reserved for a public +registry and is not implemented. It will require `componentVersion`. Until it +is specified, a reference of that shape matches no form and is rejected. + +### 4.2 Connections Connections are **consumer-anchored**: the node that needs a value declares where it comes from. A component never declares who consumes it. @@ -91,7 +179,11 @@ An empty `parameters` mapping is not the same as an absent one. As defined in [component §7](../../component/v1/spec.md#validation-layers). Blueprint documents exercise the `semantic` phase more heavily than any other -family — reference resolution and cycle detection both live there. +family — repo-local reference resolution and cycle detection both live there. + +Published reference resolution is the exception: it needs the catalog, so it +belongs to `capability`. A blueprint composed entirely of repo-local references +therefore validates completely offline, all the way through `semantic`. ## 7. Diagnostics @@ -100,6 +192,9 @@ family adds: | Code | Phase | Meaning | |---|---|---| +| `ERR_COMPONENT_NOT_FOUND` | `semantic` | A repo-local `component` reference resolves to no document. | +| `ERR_REFERENCE_ESCAPE` | `semantic` | A repo-local `component` reference resolves outside the item root. | +| `ERR_UNKNOWN_COMPONENT` | `capability` | A published `component` reference names no component, or no such `componentVersion`. | | `ERR_UNKNOWN_ROLE` | `semantic` | A connection's `fromRole` names no node in this blueprint. | | `ERR_UNKNOWN_OUTPUT` | `semantic` | A connection's `fromOutput` names no output of the referenced component. | | `ERR_DEPENDENCY_CYCLE` | `semantic` | The connection graph contains a cycle. | diff --git a/tools/src/catalog.ts b/tools/src/catalog.ts index 9d44d76..c8aa200 100644 --- a/tools/src/catalog.ts +++ b/tools/src/catalog.ts @@ -12,9 +12,24 @@ import { canonicalJson, discoverFamilies, type Json, REPO_ROOT } from './spec.ts const CATALOG_PATH = join(REPO_ROOT, 'catalog.json') -/** Glob patterns an editor uses to bind a family's schema to a file. */ +/** + * Glob patterns an editor uses to bind a family's schema to a file. + * + * A blueprint's `component` reference is a relative path, so a component + * document is not obliged to live under `components/` — a flat directory + * beside the blueprint is equally valid. The component patterns cover both; + * binding only the directory form would leave the flat layout with no editor + * support for the exact layout the reference form exists to permit. + */ const FILE_MATCH: Record = { - component: ['**/components/*.yaml', '**/components/*.yml'], + component: [ + '**/components/*.yaml', + '**/components/*.yml', + '**/component.yaml', + '**/component.yml', + '**/component-*.yaml', + '**/component-*.yml', + ], blueprint: ['**/blueprint.yaml', '**/blueprint.yml'], listing: ['**/listing.yaml', '**/listing.yml'], }