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); });