From 0d8abb57842c8cf4c768628d8efbfae2a243b9ee Mon Sep 17 00:00:00 2001 From: Justin Merrell Date: Tue, 15 Sep 2026 19:03:01 +0000 Subject: [PATCH] fix(items): re-spell the corpus for spec #93 and enforce LIST-ITEM-001 musher-dev/specifications#93 merged on 2026-09-15 and is served from the v1 alias already, so every listing in the corpus failed the structural phase that day. ADR 0025 removes the listing's revision, renames listingKind to itemType and binds it to the item, and applies ADR 0007's URL casing. Items and harness land together: neither half goes green without the other. Items, mechanical and with no behaviour change, so no revision is bumped: - metadata.revision is dropped from all 14 listings - spec.listingKind becomes spec.itemType - homepageUrl, sourceRepoUrl, supportUrl become homepageURL, sourceRepoURL, supportURL - license takes the SPDX expression grammar: n8n is SUL-1.0, redis is LicenseRef-RSALv2 OR SSPL-1.0 OR AGPL-3.0-only, open-webui is LicenseRef-Open-WebUI postgres and redis lose their one-node blueprint.yaml. LIST-ITEM-001 makes itemType BLUEPRINT exactly when the item holds one, so a COMPONENT item can no longer carry it. They become building blocks like llm-endpoint, and no longer deploy on their own. Harness: - checkIdentity reports ERR_SLUG_MISMATCH only; CORE-ITEM-002 and ERR_VERSION_MISMATCH are withdrawn - checkItemType adds LIST-ITEM-001 / ERR_ITEM_TYPE_MISMATCH at /spec/itemType - layout.test.ts drops its catalog-only blueprint rule, which LIST-ITEM-001 now states in both directions - structural.test.ts skips the blueprint case for any item holding none - rules.test.ts moves to the new listing shape and gains both directions of the new rule plus a clean COMPONENT item that validates structurally Refs musher-dev/specifications#93. Co-Authored-By: Claude Opus 5 (1M context) --- README.md | 38 ++++++++++++++------------ items/code-server/listing.yaml | 9 +++---- items/flowise/listing.yaml | 9 +++---- items/label-studio/listing.yaml | 9 +++---- items/langflow/listing.yaml | 9 +++---- items/litellm/listing.yaml | 9 +++---- items/llm-endpoint/listing.yaml | 3 +-- items/meilisearch/listing.yaml | 9 +++---- items/mlflow/listing.yaml | 9 +++---- items/n8n/listing.yaml | 11 ++++---- items/open-webui/listing.yaml | 11 ++++---- items/openclaw/listing.yaml | 9 +++---- items/postgres/blueprint.yaml | 12 --------- items/postgres/listing.yaml | 7 +++-- items/qdrant/listing.yaml | 9 +++---- items/redis/blueprint.yaml | 12 --------- items/redis/listing.yaml | 9 +++---- tests/README.md | 2 +- tests/layout.test.ts | 21 +++------------ tests/lib/semantic.ts | 47 +++++++++++++++++++++------------ tests/rules.test.ts | 28 +++++++++++++++++--- tests/semantic.test.ts | 8 +++++- tests/structural.test.ts | 10 +++---- 23 files changed, 145 insertions(+), 155 deletions(-) delete mode 100644 items/postgres/blueprint.yaml delete mode 100644 items/redis/blueprint.yaml diff --git a/README.md b/README.md index 073bf9f..1613be5 100644 --- a/README.md +++ b/README.md @@ -22,7 +22,7 @@ normatively in items/ └── / # ONE self-contained item per directory ├── listing.yaml # storefront wrapper - ├── blueprint.yaml # composition graph (see COMPONENT items below) + ├── blueprint.yaml # composition graph; absent for a COMPONENT item ├── components/ │ └── .yaml # ≥1; every file referenced by blueprint.yaml └── media/ # optional assets @@ -38,7 +38,9 @@ These are hard requirements. A violation is rejected when the platform syncs this repo: - the directory name equals both `listing.yaml`'s and `blueprint.yaml`'s - `metadata.slug`, and their `metadata.revision` values match; + `metadata.slug`; +- the listing's `spec.itemType` is `BLUEPRINT` exactly when the item holds a + `blueprint.yaml`, and `COMPONENT` otherwise; - every blueprint node's `componentRef` resolves to a `components/.yaml` file **in the same item directory**, and every such file is referenced — no unreferenced components; @@ -52,8 +54,9 @@ this repo: a readiness probe for a public endpoint; `WORKER`, `JOB` and `CRON` forbid endpoints. -Per the spec, a listing deploys exactly one blueprint, and compute is a -per-node concern on the blueprint node rather than on the component. +Per the spec, a `BLUEPRINT` item deploys exactly one blueprint, and compute is a +per-node concern on the blueprint node rather than on the component. The +blueprint's `metadata.revision` is the item's revision; a listing carries none. ## Adding an item @@ -98,7 +101,7 @@ binds compute per node: ```yaml specVersion: v1 kind: BLUEPRINT -metadata: { slug: my-app, revision: 1 } +metadata: { slug: my-app, revision: 1 } # the item's revision spec: components: web: # graph-local node name (map order = graph order) @@ -117,9 +120,9 @@ validator could tell which resolver the reference wanted. ```yaml specVersion: v1 kind: LISTING -metadata: { slug: my-app, revision: 1 } +metadata: { slug: my-app } # slug only — a listing carries no revision spec: - listingKind: BLUEPRINT # BLUEPRINT | COMPONENT + itemType: BLUEPRINT # BLUEPRINT iff the item holds blueprint.yaml displayName: My App summary: One-line storefront tagline (≤ 280 chars) description: | @@ -127,9 +130,9 @@ spec: category: DEVELOPER_TOOLS lifecycleStage: STABLE # STABLE | BETA | EXPERIMENTAL | SUNSET tags: [example] - homepageUrl: https://example.com - sourceRepoUrl: https://github.com/example/my-app - license: MIT + homepageURL: https://example.com + sourceRepoURL: https://github.com/example/my-app + license: MIT # SPDX expression; LicenseRef-… when SPDX has none icon: media/icon.png # optional; see ICONS.md screenshots: # optional; {file, caption?} in display order - file: media/screenshots/01-home.png @@ -149,13 +152,14 @@ language-model endpoint — is a component declaring `spec.external` in place of `spec.workload`. Its blueprint node writes `size: null`, and the values it holds reach the install form through its `USER` inputs like any other node's. -A `COMPONENT`-kind listing that wraps a workload still authors a trivial -single-node `blueprint.yaml` around its one component, so it can be deployed on -its own (`postgres`, `redis`). One that publishes an external building block — -`llm-endpoint` — holds **no** `blueprint.yaml`, as listing spec §3.1 permits: a -one-node blueprint around a node that runs nothing would deploy nothing. A -blueprint that needs such a node carries its own copy under `components/`, -because a repo-local reference cannot leave its item directory. +An item holding **no** `blueprint.yaml` is an `itemType: COMPONENT` item: a +single building block rather than a composition — `postgres` and `redis`, which +wrap a workload, and `llm-endpoint`, which runs nothing. Listing spec §3 binds +the two together, so a `COMPONENT` item cannot carry a blueprint and a +`BLUEPRINT` item cannot omit one. Such an item has no item revision; its +component documents carry their own. A blueprint that needs one of these +building blocks carries its own copy under `components/`, because a repo-local +reference cannot leave its item directory. ## Validation diff --git a/items/code-server/listing.yaml b/items/code-server/listing.yaml index 9ff54b8..f36cd72 100755 --- a/items/code-server/listing.yaml +++ b/items/code-server/listing.yaml @@ -2,9 +2,8 @@ specVersion: v1 kind: LISTING metadata: slug: code-server - revision: 1 spec: - listingKind: BLUEPRINT + itemType: BLUEPRINT displayName: code-server summary: VS Code in the browser, running on infrastructure you control description: | @@ -36,8 +35,8 @@ spec: - vscode - editor - remote-development - homepageUrl: https://coder.com - sourceRepoUrl: https://github.com/coder/code-server - supportUrl: https://coder.com/docs/code-server + homepageURL: https://coder.com + sourceRepoURL: https://github.com/coder/code-server + supportURL: https://coder.com/docs/code-server license: MIT icon: media/icon.png diff --git a/items/flowise/listing.yaml b/items/flowise/listing.yaml index 4a3a1f9..e113ead 100644 --- a/items/flowise/listing.yaml +++ b/items/flowise/listing.yaml @@ -2,9 +2,8 @@ specVersion: v1 kind: LISTING metadata: slug: flowise - revision: 1 spec: - listingKind: BLUEPRINT + itemType: BLUEPRINT displayName: Flowise summary: Drag-and-drop builder that turns LLM chains and agents into APIs and chat widgets description: | @@ -42,8 +41,8 @@ spec: - rag - low-code - visual-builder - homepageUrl: https://flowiseai.com - sourceRepoUrl: https://github.com/FlowiseAI/Flowise - supportUrl: https://docs.flowiseai.com + homepageURL: https://flowiseai.com + sourceRepoURL: https://github.com/FlowiseAI/Flowise + supportURL: https://docs.flowiseai.com license: Apache-2.0 icon: media/icon.png diff --git a/items/label-studio/listing.yaml b/items/label-studio/listing.yaml index 777a0ef..85da86a 100755 --- a/items/label-studio/listing.yaml +++ b/items/label-studio/listing.yaml @@ -2,9 +2,8 @@ specVersion: v1 kind: LISTING metadata: slug: label-studio - revision: 2 spec: - listingKind: BLUEPRINT + itemType: BLUEPRINT displayName: Label Studio summary: Open-source data labeling for text, image, audio, video, and time-series datasets description: | @@ -44,8 +43,8 @@ spec: - annotation - training-data - ml - homepageUrl: https://labelstud.io - sourceRepoUrl: https://github.com/HumanSignal/label-studio - supportUrl: https://labelstud.io/guide + homepageURL: https://labelstud.io + sourceRepoURL: https://github.com/HumanSignal/label-studio + supportURL: https://labelstud.io/guide license: Apache-2.0 icon: media/icon.png diff --git a/items/langflow/listing.yaml b/items/langflow/listing.yaml index 81b020a..97aeb7e 100755 --- a/items/langflow/listing.yaml +++ b/items/langflow/listing.yaml @@ -2,9 +2,8 @@ specVersion: v1 kind: LISTING metadata: slug: langflow - revision: 1 spec: - listingKind: BLUEPRINT + itemType: BLUEPRINT displayName: Langflow summary: Visual canvas for prototyping LLM agents and RAG pipelines, exportable as APIs description: | @@ -43,8 +42,8 @@ spec: - rag - low-code - visual-builder - homepageUrl: https://www.langflow.org - sourceRepoUrl: https://github.com/langflow-ai/langflow - supportUrl: https://docs.langflow.org + homepageURL: https://www.langflow.org + sourceRepoURL: https://github.com/langflow-ai/langflow + supportURL: https://docs.langflow.org license: MIT icon: media/icon.png diff --git a/items/litellm/listing.yaml b/items/litellm/listing.yaml index f71c316..6a1a747 100755 --- a/items/litellm/listing.yaml +++ b/items/litellm/listing.yaml @@ -2,9 +2,8 @@ specVersion: v1 kind: LISTING metadata: slug: litellm - revision: 1 spec: - listingKind: BLUEPRINT + itemType: BLUEPRINT displayName: LiteLLM summary: OpenAI-compatible proxy that fronts 100+ LLM providers with keys, budgets, and logging description: | @@ -41,8 +40,8 @@ spec: - proxy - gateway - openai-compatible - homepageUrl: https://www.litellm.ai - sourceRepoUrl: https://github.com/BerriAI/litellm - supportUrl: https://docs.litellm.ai + homepageURL: https://www.litellm.ai + sourceRepoURL: https://github.com/BerriAI/litellm + supportURL: https://docs.litellm.ai license: MIT icon: media/icon.png diff --git a/items/llm-endpoint/listing.yaml b/items/llm-endpoint/listing.yaml index f1df30d..35316a9 100644 --- a/items/llm-endpoint/listing.yaml +++ b/items/llm-endpoint/listing.yaml @@ -2,9 +2,8 @@ specVersion: v1 kind: LISTING metadata: slug: llm-endpoint - revision: 1 spec: - listingKind: COMPONENT + itemType: COMPONENT displayName: LLM Endpoint summary: >- A language-model API you already have, as a building block other items can diff --git a/items/meilisearch/listing.yaml b/items/meilisearch/listing.yaml index a0125bc..6765756 100755 --- a/items/meilisearch/listing.yaml +++ b/items/meilisearch/listing.yaml @@ -2,9 +2,8 @@ specVersion: v1 kind: LISTING metadata: slug: meilisearch - revision: 1 spec: - listingKind: BLUEPRINT + itemType: BLUEPRINT displayName: Meilisearch summary: Millisecond full-text search engine with typo tolerance, facets, and filters description: | @@ -37,8 +36,8 @@ spec: - full-text - indexing - typo-tolerance - homepageUrl: https://www.meilisearch.com - sourceRepoUrl: https://github.com/meilisearch/meilisearch - supportUrl: https://www.meilisearch.com/docs + homepageURL: https://www.meilisearch.com + sourceRepoURL: https://github.com/meilisearch/meilisearch + supportURL: https://www.meilisearch.com/docs license: MIT icon: media/icon.png diff --git a/items/mlflow/listing.yaml b/items/mlflow/listing.yaml index 40efca8..64ad519 100644 --- a/items/mlflow/listing.yaml +++ b/items/mlflow/listing.yaml @@ -2,9 +2,8 @@ specVersion: v1 kind: LISTING metadata: slug: mlflow - revision: 2 spec: - listingKind: BLUEPRINT + itemType: BLUEPRINT displayName: MLflow summary: Experiment tracking and model registry for ML teams — runs, metrics, artifacts description: | @@ -39,8 +38,8 @@ spec: - mlops - experiment-tracking - model-registry - homepageUrl: https://mlflow.org - sourceRepoUrl: https://github.com/mlflow/mlflow - supportUrl: https://mlflow.org/docs + homepageURL: https://mlflow.org + sourceRepoURL: https://github.com/mlflow/mlflow + supportURL: https://mlflow.org/docs license: Apache-2.0 icon: media/icon.png diff --git a/items/n8n/listing.yaml b/items/n8n/listing.yaml index 17f43a0..25f4cbe 100644 --- a/items/n8n/listing.yaml +++ b/items/n8n/listing.yaml @@ -2,9 +2,8 @@ specVersion: v1 kind: LISTING metadata: slug: n8n - revision: 1 spec: - listingKind: BLUEPRINT + itemType: BLUEPRINT displayName: n8n summary: Fair-code workflow automation with 400+ integrations and a built-in code node description: | @@ -39,8 +38,8 @@ spec: - workflow - integration - low-code - homepageUrl: https://n8n.io - sourceRepoUrl: https://github.com/n8n-io/n8n - supportUrl: https://docs.n8n.io - license: Sustainable Use License + homepageURL: https://n8n.io + sourceRepoURL: https://github.com/n8n-io/n8n + supportURL: https://docs.n8n.io + license: SUL-1.0 icon: media/icon.png diff --git a/items/open-webui/listing.yaml b/items/open-webui/listing.yaml index 50d83af..f9bfe78 100644 --- a/items/open-webui/listing.yaml +++ b/items/open-webui/listing.yaml @@ -2,9 +2,8 @@ specVersion: v1 kind: LISTING metadata: slug: open-webui - revision: 2 spec: - listingKind: BLUEPRINT + itemType: BLUEPRINT displayName: Open WebUI summary: Self-hosted, multi-user chat interface for any OpenAI-compatible language-model API description: | @@ -52,8 +51,8 @@ spec: - chat - openai-compatible - rag - homepageUrl: https://openwebui.com - sourceRepoUrl: https://github.com/open-webui/open-webui - supportUrl: https://docs.openwebui.com - license: Open WebUI License (BSD-3-Clause with branding clause) + homepageURL: https://openwebui.com + sourceRepoURL: https://github.com/open-webui/open-webui + supportURL: https://docs.openwebui.com + license: LicenseRef-Open-WebUI icon: media/icon.png diff --git a/items/openclaw/listing.yaml b/items/openclaw/listing.yaml index 2226b0e..7fb9455 100755 --- a/items/openclaw/listing.yaml +++ b/items/openclaw/listing.yaml @@ -2,9 +2,8 @@ specVersion: v1 kind: LISTING metadata: slug: openclaw - revision: 1 spec: - listingKind: BLUEPRINT + itemType: BLUEPRINT displayName: OpenClaw summary: Self-hosted AI agent gateway with a browser Control UI, bring-your-own model keys description: | @@ -63,8 +62,8 @@ spec: - assistant - gateway - automation - homepageUrl: https://openclaw.ai - sourceRepoUrl: https://github.com/openclaw/openclaw - supportUrl: https://docs.openclaw.ai + homepageURL: https://openclaw.ai + sourceRepoURL: https://github.com/openclaw/openclaw + supportURL: https://docs.openclaw.ai license: MIT icon: media/icon.png diff --git a/items/postgres/blueprint.yaml b/items/postgres/blueprint.yaml deleted file mode 100644 index 413606b..0000000 --- a/items/postgres/blueprint.yaml +++ /dev/null @@ -1,12 +0,0 @@ -specVersion: v1 -kind: BLUEPRINT -metadata: - slug: postgres - revision: 2 -spec: - components: - db: - componentRef: ./components/postgres.yaml - size: general.standard.small - connections: {} - parameters: {} diff --git a/items/postgres/listing.yaml b/items/postgres/listing.yaml index 448b64b..bd41d1b 100644 --- a/items/postgres/listing.yaml +++ b/items/postgres/listing.yaml @@ -2,9 +2,8 @@ specVersion: v1 kind: LISTING metadata: slug: postgres - revision: 2 spec: - listingKind: COMPONENT + itemType: COMPONENT displayName: PostgreSQL summary: The open-source relational database — SQL, ACID transactions, and JSONB in one engine description: | @@ -37,7 +36,7 @@ spec: - sql - postgres - relational - homepageUrl: https://www.postgresql.org - sourceRepoUrl: https://github.com/postgres/postgres + homepageURL: https://www.postgresql.org + sourceRepoURL: https://github.com/postgres/postgres license: PostgreSQL icon: media/icon.png diff --git a/items/qdrant/listing.yaml b/items/qdrant/listing.yaml index b015311..53f22f3 100755 --- a/items/qdrant/listing.yaml +++ b/items/qdrant/listing.yaml @@ -2,9 +2,8 @@ specVersion: v1 kind: LISTING metadata: slug: qdrant - revision: 2 spec: - listingKind: BLUEPRINT + itemType: BLUEPRINT displayName: Qdrant summary: Rust vector database for similarity search and RAG retrieval on CPU hardware description: | @@ -46,8 +45,8 @@ spec: - similarity-search - rag - ai - homepageUrl: https://qdrant.tech - sourceRepoUrl: https://github.com/qdrant/qdrant - supportUrl: https://qdrant.tech/documentation + homepageURL: https://qdrant.tech + sourceRepoURL: https://github.com/qdrant/qdrant + supportURL: https://qdrant.tech/documentation license: Apache-2.0 icon: media/icon.png diff --git a/items/redis/blueprint.yaml b/items/redis/blueprint.yaml deleted file mode 100644 index 6c709f2..0000000 --- a/items/redis/blueprint.yaml +++ /dev/null @@ -1,12 +0,0 @@ -specVersion: v1 -kind: BLUEPRINT -metadata: - slug: redis - revision: 1 -spec: - components: - cache: - componentRef: ./components/redis.yaml - size: general.standard.small - connections: {} - parameters: {} diff --git a/items/redis/listing.yaml b/items/redis/listing.yaml index 2507771..d89adfa 100644 --- a/items/redis/listing.yaml +++ b/items/redis/listing.yaml @@ -2,9 +2,8 @@ specVersion: v1 kind: LISTING metadata: slug: redis - revision: 1 spec: - listingKind: COMPONENT + itemType: COMPONENT displayName: Redis summary: In-memory data store for caching, queues, sessions, and pub/sub description: | @@ -35,7 +34,7 @@ spec: - key-value - redis - message-broker - homepageUrl: https://redis.io - sourceRepoUrl: https://github.com/redis/redis - license: RSALv2 / SSPLv1 / AGPL-3.0 + homepageURL: https://redis.io + sourceRepoURL: https://github.com/redis/redis + license: LicenseRef-RSALv2 OR SSPL-1.0 OR AGPL-3.0-only icon: media/icon.png diff --git a/tests/README.md b/tests/README.md index d2b9f02..f21672a 100644 --- a/tests/README.md +++ b/tests/README.md @@ -53,7 +53,7 @@ phases pass. | `spec.test.ts` | — | The bundles resolve, name their own family, and are self-contained. Fails first, so a corpus is never judged against a 404 page. | | `parser.test.ts` | `parser` | Every document satisfies the Musher YAML profile (core §6.1): one document per file, string keys, no anchors, aliases, merge keys or explicit tags, and the size, depth and scalar bounds. | | `structural.test.ts` | `structural` | Every document validates against its family's fetched JSON Schema. | -| `semantic.test.ts` | `semantic` | The cross-document rules: identity agreement, reference resolution, path containment, media, the description Markdown profile, image pinning, endpoint resolution, `INPUT` output references (COMP-OUT-002/003), node compute against external components (BP-NODE-002), connection compatibility including which inputs a wire may fill (BP-CONN-001), and parameter coverage and agreement. | +| `semantic.test.ts` | `semantic` | The cross-document rules: identity agreement, the listing's `itemType` against the item root (LIST-ITEM-001), reference resolution, path containment, media, the description Markdown profile, image pinning, endpoint resolution, `INPUT` output references (COMP-OUT-002/003), node compute against external components (BP-NODE-002), connection compatibility including which inputs a wire may fill (BP-CONN-001), and parameter coverage and agreement. | | `layout.test.ts` | — | The item folder structure, and the catalog's own additions to it. | | `rules.test.ts` | — | The rules themselves, against deliberately broken synthetic items. | diff --git a/tests/layout.test.ts b/tests/layout.test.ts index e5cf7bf..1873f80 100644 --- a/tests/layout.test.ts +++ b/tests/layout.test.ts @@ -7,6 +7,9 @@ * `blueprint.yaml`, `listing.yaml`, and `media/` — so this file tests those and * the catalog's own additions on top of them, which the repository README states * as hard requirements for a platform sync. + * + * Which items hold a blueprint is not tested here. LIST-ITEM-001 binds it to + * the listing's `itemType` in both directions, so the semantic phase decides it. */ import fs from 'node:fs'; import path from 'node:path'; @@ -20,7 +23,6 @@ import { MEDIA_DIR, SLUG_PATTERN, discoverItems, - loadItemDocuments, } from './lib/catalog.ts'; import { ITEMS_DIR, rel } from './lib/paths.ts'; @@ -65,23 +67,6 @@ for (const item of items) { assert.ok(item.listingPath, `${rel(item.root)} holds no ${LISTING_FILE}`); }); - it(`holds ${BLUEPRINT_FILE}, unless it is a COMPONENT item`, async () => { - // Listing spec §3.1 lets a COMPONENT-kind item ship with no blueprint, and - // this corpus narrows that to a catalog rule: a BLUEPRINT item must hold - // one. A COMPONENT item wrapping a workload still authors a trivial - // single-node blueprint so it is deployable on its own (postgres, redis). - // An external building block does not, because a one-node blueprint around - // a node that runs nothing would deploy nothing. - if (item.blueprintPath) return; - const { listing } = await loadItemDocuments(item); - const listingKind = (listing?.value?.['spec'] as Record | undefined)?.['listingKind']; - assert.equal( - listingKind, - 'COMPONENT', - `${rel(item.root)} holds no ${BLUEPRINT_FILE}, and only a listingKind: COMPONENT item may omit one`, - ); - }); - it('holds at least one component document', () => { assert.ok(item.componentPaths.length > 0, `${rel(item.root)} holds no component document`); }); diff --git a/tests/lib/semantic.ts b/tests/lib/semantic.ts index 1dac0d9..d8c1122 100644 --- a/tests/lib/semantic.ts +++ b/tests/lib/semantic.ts @@ -129,7 +129,7 @@ export function buildContext( /* ---------------------------------------------------------------- identity */ -/** CORE-ITEM-001, CORE-ITEM-002. */ +/** CORE-ITEM-001. */ export function checkIdentity(context: SemanticContext): Diagnostic[] { const { item, documents } = context; const found: Diagnostic[] = []; @@ -151,25 +151,38 @@ export function checkIdentity(context: SemanticContext): Diagnostic[] { } } - // The rule takes two operands. A COMPONENT item holding no blueprint has no - // second one — not a different one — so it goes silent rather than failing. - // The field is `revision` and the code is still ERR_VERSION_MISMATCH: ADR 0007 - // §3 renamed the field and left the diagnostic, which core spec §7's table keeps. - const listingRevision = metadataOf(documents.listing)['revision']; - const blueprintRevision = metadataOf(documents.blueprint)['revision']; - if (documents.listing?.value && documents.blueprint?.value && listingRevision !== blueprintRevision) { - found.push( - diag( - 'ERR_VERSION_MISMATCH', - `${documents.blueprint.label} /metadata/revision`, - `blueprint revision ${JSON.stringify(blueprintRevision)} disagrees with listing revision ${JSON.stringify(listingRevision)}`, - ), - ); - } - return found; } +/** + * LIST-ITEM-001 — listing spec §3. `spec.itemType` is `BLUEPRINT` if and only if + * the item root holds `blueprint.yaml`. + * + * It reads the directory rather than the document, which is what makes it + * `semantic`. Holding the file is what counts, not whether it parses: a broken + * blueprint is still a blueprint, and the parser phase reports it. + */ +export function checkItemType(context: SemanticContext): Diagnostic[] { + const listing = context.documents.listing; + if (!listing?.value) return []; + + const declared = specOf(listing)['itemType']; + if (typeof declared !== 'string') return []; + + const holdsBlueprint = context.item.blueprintPath !== null; + if (declared === (holdsBlueprint ? 'BLUEPRINT' : 'COMPONENT')) return []; + + return [ + diag( + 'ERR_ITEM_TYPE_MISMATCH', + `${listing.label} /spec/itemType`, + holdsBlueprint + ? `itemType is ${declared}, but the item root holds a blueprint.yaml` + : `itemType is ${declared}, but the item root holds no blueprint.yaml`, + ), + ]; +} + /* -------------------------------------------------------------- references */ /** Blueprint spec §4.1 and BP-ID-003. */ diff --git a/tests/rules.test.ts b/tests/rules.test.ts index 65ad588..7dc7cae 100644 --- a/tests/rules.test.ts +++ b/tests/rules.test.ts @@ -28,6 +28,7 @@ import { checkHealthProbes, checkIdentity, checkImagePinning, + checkItemType, checkMedia, checkNodeCompute, checkOutputInputReferences, @@ -48,9 +49,9 @@ type Doc = Record; const listing = (over: Doc = {}): Doc => ({ specVersion: 'v1', kind: 'LISTING', - metadata: { slug: 'acme-wiki', revision: 1 }, + metadata: { slug: 'acme-wiki' }, spec: { - listingKind: 'BLUEPRINT', + itemType: 'BLUEPRINT', displayName: 'Acme Wiki', summary: 'A wiki', description: 'Long-form copy.', @@ -155,6 +156,7 @@ async function diagnose(root: string): Promise { return [ ...checkIdentity(context), + ...checkItemType(context), ...checkComponentReferences(context), ...checkMedia(context), ...checkDescription(context), @@ -224,9 +226,27 @@ describe('identity — blueprint §3, listing §3', () => { it('ERR_SLUG_MISMATCH when metadata.slug disagrees with the directory name', async () => { await assertReports({ blueprint: blueprint({ metadata: { slug: 'other', revision: 1 } }) }, 'ERR_SLUG_MISMATCH'); }); +}); + +describe('item type — listing §3, LIST-ITEM-001', () => { + const componentItem = { blueprint: null, listing: listing({ spec: { itemType: 'COMPONENT' } }) }; + + it('ERR_ITEM_TYPE_MISMATCH when a BLUEPRINT listing sits in an item holding no blueprint', async () => { + // spec listing conformance semantic/015: a composition nobody can install. + await assertReports({ blueprint: null }, 'ERR_ITEM_TYPE_MISMATCH'); + }); + + it('ERR_ITEM_TYPE_MISMATCH when a COMPONENT listing sits beside a blueprint', async () => { + // spec listing conformance semantic/016. + await assertReports({ listing: listing({ spec: { itemType: 'COMPONENT' } }) }, 'ERR_ITEM_TYPE_MISMATCH'); + }); + + it('accepts a COMPONENT item holding no blueprint', async () => { + await assertClean(componentItem); + }); - it('ERR_VERSION_MISMATCH when the two halves of the item disagree', async () => { - await assertReports({ blueprint: blueprint({ metadata: { slug: 'acme-wiki', revision: 2 } }) }, 'ERR_VERSION_MISMATCH'); + it('the accepted COMPONENT item is a real item', async () => { + await assertStructurallyValid(build(componentItem)); }); }); diff --git a/tests/semantic.test.ts b/tests/semantic.test.ts index 9ba06a6..0042bf1 100644 --- a/tests/semantic.test.ts +++ b/tests/semantic.test.ts @@ -26,6 +26,7 @@ import { checkHealthProbes, checkIdentity, checkImagePinning, + checkItemType, checkMedia, checkNodeCompute, checkOutputInputReferences, @@ -66,11 +67,16 @@ const report = (diagnostics: Diagnostic[]): string[] => for (const item of items) { describe(`items/${item.slug}`, () => { - it('slug and version agree across the item — CORE-ITEM-001/002', async () => { + it('slug agrees with the item directory — CORE-ITEM-001', async () => { const { context } = await contextFor(item); assert.deepEqual(report(checkIdentity(context)), []); }); + it('itemType agrees with what the item root holds — LIST-ITEM-001', async () => { + const { context } = await contextFor(item); + assert.deepEqual(report(checkItemType(context)), []); + }); + it('every component reference resolves to a document inside the item root — §4.1, BP-ID-003', async () => { const { context } = await contextFor(item); assert.deepEqual(report(checkComponentReferences(context)), []); diff --git a/tests/structural.test.ts b/tests/structural.test.ts index cdf4d7a..fc9c9ba 100644 --- a/tests/structural.test.ts +++ b/tests/structural.test.ts @@ -40,13 +40,13 @@ for (const item of items) { }); it('blueprint.yaml validates against the blueprint schema', async (t) => { - const { blueprint, listing } = await loadItemDocuments(item); - if (!blueprint && (listing?.value?.['spec'] as Record | undefined)?.['listingKind'] === 'COMPONENT') { - // Listing spec §3.1: a COMPONENT item may hold no blueprint. Whether - // this one may is layout.test.ts's question, not this phase's. - t.skip('a COMPONENT item holding no blueprint'); + if (!item.blueprintPath) { + // Listing spec §3.1: an item may hold no blueprint. Whether this one + // may is LIST-ITEM-001's question, which the semantic phase answers. + t.skip('an item holding no blueprint'); return; } + const { blueprint } = await loadItemDocuments(item); assert.ok(blueprint, `items/${item.slug} holds no blueprint.yaml`); await assertValidates('blueprint', blueprint); });