From 3664148c2ebb6c573c3bd376be7dcd9d22645e6a Mon Sep 17 00:00:00 2001 From: benjdlambert Date: Fri, 15 May 2026 08:49:01 +0200 Subject: [PATCH 01/15] feat: add AIResource catalog entity kind as opt-in module Introduces @backstage/plugin-catalog-backend-module-ai-resource-entity-model, a new backend module that registers the AIResource kind with the catalog. Signed-off-by: benjdlambert --- .changeset/add-ai-resource-entity-model.md | 5 + .../.eslintrc.js | 1 + .../catalog-info.yaml | 10 ++ .../package.json | 58 +++++++ .../report-alpha.api.md | 12 ++ .../report.api.md | 36 +++++ .../src/AIResourceEntityV1alpha1.test.ts | 145 ++++++++++++++++++ .../src/AIResourceEntityV1alpha1.ts | 108 +++++++++++++ .../src/alpha.ts | 17 ++ .../src/index.ts | 28 ++++ .../src/module.test.ts | 36 +++++ .../src/module.ts | 42 +++++ .../schema/AIResource.v1alpha1.schema.json | 67 ++++++++ yarn.lock | 13 ++ 14 files changed, 578 insertions(+) create mode 100644 .changeset/add-ai-resource-entity-model.md create mode 100644 plugins/catalog-backend-module-ai-resource-entity-model/.eslintrc.js create mode 100644 plugins/catalog-backend-module-ai-resource-entity-model/catalog-info.yaml create mode 100644 plugins/catalog-backend-module-ai-resource-entity-model/package.json create mode 100644 plugins/catalog-backend-module-ai-resource-entity-model/report-alpha.api.md create mode 100644 plugins/catalog-backend-module-ai-resource-entity-model/report.api.md create mode 100644 plugins/catalog-backend-module-ai-resource-entity-model/src/AIResourceEntityV1alpha1.test.ts create mode 100644 plugins/catalog-backend-module-ai-resource-entity-model/src/AIResourceEntityV1alpha1.ts create mode 100644 plugins/catalog-backend-module-ai-resource-entity-model/src/alpha.ts create mode 100644 plugins/catalog-backend-module-ai-resource-entity-model/src/index.ts create mode 100644 plugins/catalog-backend-module-ai-resource-entity-model/src/module.test.ts create mode 100644 plugins/catalog-backend-module-ai-resource-entity-model/src/module.ts create mode 100644 plugins/catalog-backend-module-ai-resource-entity-model/src/schema/AIResource.v1alpha1.schema.json diff --git a/.changeset/add-ai-resource-entity-model.md b/.changeset/add-ai-resource-entity-model.md new file mode 100644 index 0000000000..9a1d853471 --- /dev/null +++ b/.changeset/add-ai-resource-entity-model.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-catalog-backend-module-ai-resource-entity-model': minor +--- + +Introduced the `AIResource` catalog entity kind as an opt-in backend module. Install this module to register support for `AIResource` entities in your catalog, used to represent contextual information consumed by AI coding tools such as skills and rules. diff --git a/plugins/catalog-backend-module-ai-resource-entity-model/.eslintrc.js b/plugins/catalog-backend-module-ai-resource-entity-model/.eslintrc.js new file mode 100644 index 0000000000..e2a53a6ad2 --- /dev/null +++ b/plugins/catalog-backend-module-ai-resource-entity-model/.eslintrc.js @@ -0,0 +1 @@ +module.exports = require('@backstage/cli/config/eslint-factory')(__dirname); diff --git a/plugins/catalog-backend-module-ai-resource-entity-model/catalog-info.yaml b/plugins/catalog-backend-module-ai-resource-entity-model/catalog-info.yaml new file mode 100644 index 0000000000..d6a29b6e18 --- /dev/null +++ b/plugins/catalog-backend-module-ai-resource-entity-model/catalog-info.yaml @@ -0,0 +1,10 @@ +apiVersion: backstage.io/v1alpha1 +kind: Component +metadata: + name: backstage-plugin-catalog-backend-module-ai-resource-entity-model + title: '@backstage/plugin-catalog-backend-module-ai-resource-entity-model' + description: Adds support for the AIResource entity kind to the catalog backend plugin. +spec: + lifecycle: experimental + type: backstage-backend-plugin-module + owner: maintainers diff --git a/plugins/catalog-backend-module-ai-resource-entity-model/package.json b/plugins/catalog-backend-module-ai-resource-entity-model/package.json new file mode 100644 index 0000000000..2ebd5b4bd6 --- /dev/null +++ b/plugins/catalog-backend-module-ai-resource-entity-model/package.json @@ -0,0 +1,58 @@ +{ + "name": "@backstage/plugin-catalog-backend-module-ai-resource-entity-model", + "version": "0.1.0", + "description": "Adds support for the AIResource entity kind to the catalog backend plugin.", + "backstage": { + "role": "backend-plugin-module", + "pluginId": "catalog", + "pluginPackage": "@backstage/plugin-catalog-backend" + }, + "publishConfig": { + "access": "public" + }, + "repository": { + "type": "git", + "url": "https://github.com/backstage/backstage", + "directory": "plugins/catalog-backend-module-ai-resource-entity-model" + }, + "license": "Apache-2.0", + "exports": { + ".": "./src/index.ts", + "./alpha": "./src/alpha.ts", + "./package.json": "./package.json" + }, + "main": "src/index.ts", + "types": "src/index.ts", + "typesVersions": { + "*": { + "alpha": [ + "src/alpha.ts" + ], + "package.json": [ + "package.json" + ] + } + }, + "files": [ + "dist" + ], + "scripts": { + "build": "backstage-cli package build", + "clean": "backstage-cli package clean", + "lint": "backstage-cli package lint", + "prepack": "backstage-cli package prepack", + "postpack": "backstage-cli package postpack", + "start": "backstage-cli package start", + "test": "backstage-cli package test" + }, + "dependencies": { + "@backstage/backend-plugin-api": "workspace:^", + "@backstage/catalog-model": "workspace:^", + "@backstage/plugin-catalog-node": "workspace:^", + "@backstage/types": "workspace:^" + }, + "devDependencies": { + "@backstage/backend-test-utils": "workspace:^", + "@backstage/cli": "workspace:^" + } +} diff --git a/plugins/catalog-backend-module-ai-resource-entity-model/report-alpha.api.md b/plugins/catalog-backend-module-ai-resource-entity-model/report-alpha.api.md new file mode 100644 index 0000000000..3012dbbfbb --- /dev/null +++ b/plugins/catalog-backend-module-ai-resource-entity-model/report-alpha.api.md @@ -0,0 +1,12 @@ +## API Report File for "@backstage/plugin-catalog-backend-module-ai-resource-entity-model" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts +import { CatalogModelLayer } from '@backstage/catalog-model/alpha'; + +// @alpha +export const aiResourceEntityModel: CatalogModelLayer; + +// (No @packageDocumentation comment for this package) +``` diff --git a/plugins/catalog-backend-module-ai-resource-entity-model/report.api.md b/plugins/catalog-backend-module-ai-resource-entity-model/report.api.md new file mode 100644 index 0000000000..6133a5d7a9 --- /dev/null +++ b/plugins/catalog-backend-module-ai-resource-entity-model/report.api.md @@ -0,0 +1,36 @@ +## API Report File for "@backstage/plugin-catalog-backend-module-ai-resource-entity-model" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts +import { BackendFeature } from '@backstage/backend-plugin-api'; +import { Entity } from '@backstage/catalog-model'; +import { KindValidator } from '@backstage/catalog-model'; + +// @public +export interface AIResourceEntityV1alpha1 extends Entity { + // (undocumented) + apiVersion: 'backstage.io/v1alpha1'; + // (undocumented) + kind: 'AIResource'; + // (undocumented) + spec: { + type: string; + lifecycle: string; + owner: string; + system?: string; + }; +} + +// @public +export const aiResourceEntityV1alpha1Validator: KindValidator; + +// @public +const catalogModuleAIResourceEntityModel: BackendFeature; +export default catalogModuleAIResourceEntityModel; + +// @public +export const isAIResourceEntity: ( + entity: Entity, +) => entity is AIResourceEntityV1alpha1; +``` diff --git a/plugins/catalog-backend-module-ai-resource-entity-model/src/AIResourceEntityV1alpha1.test.ts b/plugins/catalog-backend-module-ai-resource-entity-model/src/AIResourceEntityV1alpha1.test.ts new file mode 100644 index 0000000000..1de67dda74 --- /dev/null +++ b/plugins/catalog-backend-module-ai-resource-entity-model/src/AIResourceEntityV1alpha1.test.ts @@ -0,0 +1,145 @@ +/* + * Copyright 2026 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import type { Entity } from '@backstage/catalog-model'; +import { + type AIResourceEntityV1alpha1, + aiResourceEntityV1alpha1Validator as validator, + isAIResourceEntity, +} from './AIResourceEntityV1alpha1'; + +describe('AIResourceV1alpha1Validator', () => { + let entity: AIResourceEntityV1alpha1; + + beforeEach(() => { + entity = { + apiVersion: 'backstage.io/v1alpha1', + kind: 'AIResource', + metadata: { + name: 'frontend-design', + }, + spec: { + type: 'skill', + lifecycle: 'production', + owner: 'ai-platform-team', + system: 'ai-tooling', + }, + }; + }); + + it('accepts valid data', async () => { + await expect(validator.check(entity)).resolves.toBe(true); + }); + + it('ignores unknown apiVersion', async () => { + (entity as any).apiVersion = 'backstage.io/v1beta0'; + await expect(validator.check(entity)).resolves.toBe(false); + }); + + it('ignores unknown kind', async () => { + (entity as any).kind = 'Wizard'; + await expect(validator.check(entity)).resolves.toBe(false); + }); + + it('rejects missing type', async () => { + delete (entity as any).spec.type; + await expect(validator.check(entity)).rejects.toThrow(/type/); + }); + + it('rejects wrong type', async () => { + (entity as any).spec.type = 7; + await expect(validator.check(entity)).rejects.toThrow(/type/); + }); + + it('rejects empty type', async () => { + (entity as any).spec.type = ''; + await expect(validator.check(entity)).rejects.toThrow(/type/); + }); + + it('rejects missing lifecycle', async () => { + delete (entity as any).spec.lifecycle; + await expect(validator.check(entity)).rejects.toThrow(/lifecycle/); + }); + + it('rejects wrong lifecycle', async () => { + (entity as any).spec.lifecycle = 7; + await expect(validator.check(entity)).rejects.toThrow(/lifecycle/); + }); + + it('rejects empty lifecycle', async () => { + (entity as any).spec.lifecycle = ''; + await expect(validator.check(entity)).rejects.toThrow(/lifecycle/); + }); + + it('rejects missing owner', async () => { + delete (entity as any).spec.owner; + await expect(validator.check(entity)).rejects.toThrow(/owner/); + }); + + it('rejects wrong owner', async () => { + (entity as any).spec.owner = 7; + await expect(validator.check(entity)).rejects.toThrow(/owner/); + }); + + it('rejects empty owner', async () => { + (entity as any).spec.owner = ''; + await expect(validator.check(entity)).rejects.toThrow(/owner/); + }); + + it('accepts missing system', async () => { + delete (entity as any).spec.system; + await expect(validator.check(entity)).resolves.toBe(true); + }); + + it('rejects wrong system', async () => { + (entity as any).spec.system = 7; + await expect(validator.check(entity)).rejects.toThrow(/system/); + }); + + it('rejects empty system', async () => { + (entity as any).spec.system = ''; + await expect(validator.check(entity)).rejects.toThrow(/system/); + }); +}); + +describe('isAIResourceEntity', () => { + it('returns true for a valid AIResource entity', () => { + const entity: Entity = { + apiVersion: 'backstage.io/v1alpha1', + kind: 'AIResource', + metadata: { name: 'test' }, + }; + expect(isAIResourceEntity(entity)).toBe(true); + }); + + it('returns false for a different kind', () => { + const entity: Entity = { + apiVersion: 'backstage.io/v1alpha1', + kind: 'Component', + metadata: { name: 'test' }, + }; + expect(isAIResourceEntity(entity)).toBe(false); + }); + + it('returns false for a different apiVersion', () => { + const entity: Entity = { + apiVersion: 'backstage.io/v1beta1', + kind: 'AIResource', + metadata: { name: 'test' }, + }; + expect(isAIResourceEntity(entity)).toBe(false); + }); +}); diff --git a/plugins/catalog-backend-module-ai-resource-entity-model/src/AIResourceEntityV1alpha1.ts b/plugins/catalog-backend-module-ai-resource-entity-model/src/AIResourceEntityV1alpha1.ts new file mode 100644 index 0000000000..b272ffb02a --- /dev/null +++ b/plugins/catalog-backend-module-ai-resource-entity-model/src/AIResourceEntityV1alpha1.ts @@ -0,0 +1,108 @@ +/* + * Copyright 2026 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { + type Entity, + entityKindSchemaValidator, + type KindValidator, +} from '@backstage/catalog-model'; +import { createCatalogModelLayer } from '@backstage/catalog-model/alpha'; +import type { JsonObject } from '@backstage/types'; +import jsonSchema from './schema/AIResource.v1alpha1.schema.json'; + +/** + * Backstage catalog AIResource kind Entity. Represents contextual information + * consumed by AI coding tools, such as skills and rules. + * + * @public + */ +export interface AIResourceEntityV1alpha1 extends Entity { + apiVersion: 'backstage.io/v1alpha1'; + kind: 'AIResource'; + spec: { + type: string; + lifecycle: string; + owner: string; + system?: string; + }; +} + +const validator = entityKindSchemaValidator(jsonSchema); + +/** + * Entity data validator for {@link AIResourceEntityV1alpha1}. + * + * @public + */ +export const aiResourceEntityV1alpha1Validator: KindValidator = { + async check(data: Entity) { + return validator(data) === data; + }, +}; + +/** + * Type guard for {@link AIResourceEntityV1alpha1}. + * + * @public + */ +export const isAIResourceEntity = ( + entity: Entity, +): entity is AIResourceEntityV1alpha1 => + entity.apiVersion === 'backstage.io/v1alpha1' && entity.kind === 'AIResource'; + +/** + * Extends the catalog model with the AIResource kind. + * + * @alpha + */ +export const aiResourceEntityModel = createCatalogModelLayer({ + layerId: 'catalog.backstage.io/kind-ai-resource', + builder: model => { + model.addKind({ + group: 'backstage.io', + names: { + kind: 'AIResource', + singular: 'airesource', + plural: 'airesources', + }, + description: + 'An AI resource represents contextual information consumed by AI coding tools, such as skills and rules.', + versions: [ + { + name: 'v1alpha1', + relationFields: [ + { + selector: { path: 'spec.owner' }, + relation: 'ownedBy', + defaultKind: 'Group', + defaultNamespace: 'inherit', + allowedKinds: ['Group', 'User'], + }, + { + selector: { path: 'spec.system' }, + relation: 'partOf', + defaultKind: 'System', + defaultNamespace: 'inherit', + }, + ], + schema: { + jsonSchema: jsonSchema as JsonObject, + }, + }, + ], + }); + }, +}); diff --git a/plugins/catalog-backend-module-ai-resource-entity-model/src/alpha.ts b/plugins/catalog-backend-module-ai-resource-entity-model/src/alpha.ts new file mode 100644 index 0000000000..1878461df7 --- /dev/null +++ b/plugins/catalog-backend-module-ai-resource-entity-model/src/alpha.ts @@ -0,0 +1,17 @@ +/* + * Copyright 2026 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export { aiResourceEntityModel } from './AIResourceEntityV1alpha1'; diff --git a/plugins/catalog-backend-module-ai-resource-entity-model/src/index.ts b/plugins/catalog-backend-module-ai-resource-entity-model/src/index.ts new file mode 100644 index 0000000000..cf1b636db1 --- /dev/null +++ b/plugins/catalog-backend-module-ai-resource-entity-model/src/index.ts @@ -0,0 +1,28 @@ +/* + * Copyright 2026 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Adds support for the AIResource entity kind to the catalog backend plugin. + * + * @packageDocumentation + */ + +export type { AIResourceEntityV1alpha1 } from './AIResourceEntityV1alpha1'; +export { + aiResourceEntityV1alpha1Validator, + isAIResourceEntity, +} from './AIResourceEntityV1alpha1'; +export { catalogModuleAIResourceEntityModel as default } from './module'; diff --git a/plugins/catalog-backend-module-ai-resource-entity-model/src/module.test.ts b/plugins/catalog-backend-module-ai-resource-entity-model/src/module.test.ts new file mode 100644 index 0000000000..030c164d86 --- /dev/null +++ b/plugins/catalog-backend-module-ai-resource-entity-model/src/module.test.ts @@ -0,0 +1,36 @@ +/* + * Copyright 2026 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { startTestBackend } from '@backstage/backend-test-utils'; +import { catalogModelExtensionPoint } from '@backstage/plugin-catalog-node/alpha'; +import { catalogModuleAIResourceEntityModel } from './module'; + +describe('catalogModuleAIResourceEntityModel', () => { + it('should register the model source', async () => { + const extensionPoint = { + setFieldValidators: jest.fn(), + setEntityDataParser: jest.fn(), + addModelSource: jest.fn(), + }; + + await startTestBackend({ + extensionPoints: [[catalogModelExtensionPoint, extensionPoint]], + features: [catalogModuleAIResourceEntityModel], + }); + + expect(extensionPoint.addModelSource).toHaveBeenCalledTimes(1); + }); +}); diff --git a/plugins/catalog-backend-module-ai-resource-entity-model/src/module.ts b/plugins/catalog-backend-module-ai-resource-entity-model/src/module.ts new file mode 100644 index 0000000000..6ccab581df --- /dev/null +++ b/plugins/catalog-backend-module-ai-resource-entity-model/src/module.ts @@ -0,0 +1,42 @@ +/* + * Copyright 2026 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { createBackendModule } from '@backstage/backend-plugin-api'; +import { CatalogModelSources } from '@backstage/catalog-model/alpha'; +import { catalogModelExtensionPoint } from '@backstage/plugin-catalog-node/alpha'; +import { aiResourceEntityModel } from './AIResourceEntityV1alpha1'; + +/** + * Registers support for the AIResource entity kind in the catalog. + * + * @public + */ +export const catalogModuleAIResourceEntityModel = createBackendModule({ + pluginId: 'catalog', + moduleId: 'ai-resource-entity-model', + register(reg) { + reg.registerInit({ + deps: { + model: catalogModelExtensionPoint, + }, + async init({ model }) { + model.addModelSource( + CatalogModelSources.static([aiResourceEntityModel]), + ); + }, + }); + }, +}); diff --git a/plugins/catalog-backend-module-ai-resource-entity-model/src/schema/AIResource.v1alpha1.schema.json b/plugins/catalog-backend-module-ai-resource-entity-model/src/schema/AIResource.v1alpha1.schema.json new file mode 100644 index 0000000000..2b84c00fd1 --- /dev/null +++ b/plugins/catalog-backend-module-ai-resource-entity-model/src/schema/AIResource.v1alpha1.schema.json @@ -0,0 +1,67 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema", + "$id": "AIResourceV1alpha1", + "description": "An AI resource represents contextual information consumed by AI coding tools, such as skills and rules.", + "examples": [ + { + "apiVersion": "backstage.io/v1alpha1", + "kind": "AIResource", + "metadata": { + "name": "frontend-design", + "description": "Skill for creating production-grade frontend interfaces" + }, + "spec": { + "type": "skill", + "lifecycle": "production", + "owner": "ai-platform-team", + "system": "ai-tooling" + } + } + ], + "allOf": [ + { + "$ref": "Entity" + }, + { + "type": "object", + "required": ["spec"], + "properties": { + "apiVersion": { + "enum": ["backstage.io/v1alpha1"] + }, + "kind": { + "enum": ["AIResource"] + }, + "spec": { + "type": "object", + "required": ["type", "lifecycle", "owner"], + "properties": { + "type": { + "type": "string", + "description": "The type of AI resource.", + "examples": ["skill", "rule"], + "minLength": 1 + }, + "lifecycle": { + "type": "string", + "description": "The lifecycle state of the AI resource.", + "examples": ["experimental", "production", "deprecated"], + "minLength": 1 + }, + "owner": { + "type": "string", + "description": "An entity reference to the owner of the AI resource.", + "examples": ["ai-platform-team", "user:john.johnson"], + "minLength": 1 + }, + "system": { + "type": "string", + "description": "An entity reference to the system that the AI resource belongs to.", + "minLength": 1 + } + } + } + } + } + ] +} diff --git a/yarn.lock b/yarn.lock index 821e3bfb8c..2737bf4ac8 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4759,6 +4759,19 @@ __metadata: languageName: unknown linkType: soft +"@backstage/plugin-catalog-backend-module-ai-resource-entity-model@workspace:plugins/catalog-backend-module-ai-resource-entity-model": + version: 0.0.0-use.local + resolution: "@backstage/plugin-catalog-backend-module-ai-resource-entity-model@workspace:plugins/catalog-backend-module-ai-resource-entity-model" + dependencies: + "@backstage/backend-plugin-api": "workspace:^" + "@backstage/backend-test-utils": "workspace:^" + "@backstage/catalog-model": "workspace:^" + "@backstage/cli": "workspace:^" + "@backstage/plugin-catalog-node": "workspace:^" + "@backstage/types": "workspace:^" + languageName: unknown + linkType: soft + "@backstage/plugin-catalog-backend-module-aws@workspace:plugins/catalog-backend-module-aws": version: 0.0.0-use.local resolution: "@backstage/plugin-catalog-backend-module-aws@workspace:plugins/catalog-backend-module-aws" From 3170784f18ab9761e9e381052c81a1369950bd3d Mon Sep 17 00:00:00 2001 From: benjdlambert Date: Fri, 15 May 2026 13:47:47 +0200 Subject: [PATCH 02/15] feat: add specType dispatch with skill-specific schema Adds discriminated union on spec.type with specType dispatch in the model layer. The skill type adds disciplines, categories, agents, and dependsOn fields. Fixes model source to not duplicate the default catalog entity model. Signed-off-by: benjdlambert --- .../report.api.md | 34 ++- .../src/AIResourceEntityV1alpha1.test.ts | 230 +++++++++++++----- .../src/AIResourceEntityV1alpha1.ts | 98 +++++++- .../src/index.ts | 8 +- .../src/module.ts | 9 +- .../AIResource.v1alpha1.skill.schema.json | 101 ++++++++ 6 files changed, 409 insertions(+), 71 deletions(-) create mode 100644 plugins/catalog-backend-module-ai-resource-entity-model/src/schema/AIResource.v1alpha1.skill.schema.json diff --git a/plugins/catalog-backend-module-ai-resource-entity-model/report.api.md b/plugins/catalog-backend-module-ai-resource-entity-model/report.api.md index 6133a5d7a9..6e62498abe 100644 --- a/plugins/catalog-backend-module-ai-resource-entity-model/report.api.md +++ b/plugins/catalog-backend-module-ai-resource-entity-model/report.api.md @@ -8,7 +8,12 @@ import { Entity } from '@backstage/catalog-model'; import { KindValidator } from '@backstage/catalog-model'; // @public -export interface AIResourceEntityV1alpha1 extends Entity { +export type AIResourceEntityV1alpha1 = + | AIResourceEntityV1alpha1Default + | SkillAIResourceEntityV1alpha1; + +// @public +export interface AIResourceEntityV1alpha1Default extends Entity { // (undocumented) apiVersion: 'backstage.io/v1alpha1'; // (undocumented) @@ -33,4 +38,31 @@ export default catalogModuleAIResourceEntityModel; export const isAIResourceEntity: ( entity: Entity, ) => entity is AIResourceEntityV1alpha1; + +// @public +export const isSkillAIResourceEntity: ( + entity: Entity, +) => entity is SkillAIResourceEntityV1alpha1; + +// @public +export interface SkillAIResourceEntityV1alpha1 extends Entity { + // (undocumented) + apiVersion: 'backstage.io/v1alpha1'; + // (undocumented) + kind: 'AIResource'; + // (undocumented) + spec: { + type: 'skill'; + lifecycle: string; + owner: string; + system?: string; + disciplines?: string[]; + categories?: string[]; + agents?: string[]; + dependsOn?: string[]; + }; +} + +// @public +export const skillAIResourceEntityV1alpha1Validator: KindValidator; ``` diff --git a/plugins/catalog-backend-module-ai-resource-entity-model/src/AIResourceEntityV1alpha1.test.ts b/plugins/catalog-backend-module-ai-resource-entity-model/src/AIResourceEntityV1alpha1.test.ts index 1de67dda74..8acbc5bc4a 100644 --- a/plugins/catalog-backend-module-ai-resource-entity-model/src/AIResourceEntityV1alpha1.test.ts +++ b/plugins/catalog-backend-module-ai-resource-entity-model/src/AIResourceEntityV1alpha1.test.ts @@ -16,13 +16,110 @@ import type { Entity } from '@backstage/catalog-model'; import { - type AIResourceEntityV1alpha1, - aiResourceEntityV1alpha1Validator as validator, + type AIResourceEntityV1alpha1Default, + type SkillAIResourceEntityV1alpha1, + aiResourceEntityV1alpha1Validator as defaultValidator, + skillAIResourceEntityV1alpha1Validator as skillValidator, isAIResourceEntity, + isSkillAIResourceEntity, } from './AIResourceEntityV1alpha1'; -describe('AIResourceV1alpha1Validator', () => { - let entity: AIResourceEntityV1alpha1; +describe('AIResourceV1alpha1 default validator', () => { + let entity: AIResourceEntityV1alpha1Default; + + beforeEach(() => { + entity = { + apiVersion: 'backstage.io/v1alpha1', + kind: 'AIResource', + metadata: { + name: 'internal-design-system', + }, + spec: { + type: 'rule', + lifecycle: 'production', + owner: 'frontend-platform', + system: 'ai-tooling', + }, + }; + }); + + it('accepts valid data', async () => { + await expect(defaultValidator.check(entity)).resolves.toBe(true); + }); + + it('ignores unknown apiVersion', async () => { + (entity as any).apiVersion = 'backstage.io/v1beta0'; + await expect(defaultValidator.check(entity)).resolves.toBe(false); + }); + + it('ignores unknown kind', async () => { + (entity as any).kind = 'Wizard'; + await expect(defaultValidator.check(entity)).resolves.toBe(false); + }); + + it('rejects missing type', async () => { + delete (entity as any).spec.type; + await expect(defaultValidator.check(entity)).rejects.toThrow(/type/); + }); + + it('rejects wrong type', async () => { + (entity as any).spec.type = 7; + await expect(defaultValidator.check(entity)).rejects.toThrow(/type/); + }); + + it('rejects empty type', async () => { + (entity as any).spec.type = ''; + await expect(defaultValidator.check(entity)).rejects.toThrow(/type/); + }); + + it('rejects missing lifecycle', async () => { + delete (entity as any).spec.lifecycle; + await expect(defaultValidator.check(entity)).rejects.toThrow(/lifecycle/); + }); + + it('rejects wrong lifecycle', async () => { + (entity as any).spec.lifecycle = 7; + await expect(defaultValidator.check(entity)).rejects.toThrow(/lifecycle/); + }); + + it('rejects empty lifecycle', async () => { + (entity as any).spec.lifecycle = ''; + await expect(defaultValidator.check(entity)).rejects.toThrow(/lifecycle/); + }); + + it('rejects missing owner', async () => { + delete (entity as any).spec.owner; + await expect(defaultValidator.check(entity)).rejects.toThrow(/owner/); + }); + + it('rejects wrong owner', async () => { + (entity as any).spec.owner = 7; + await expect(defaultValidator.check(entity)).rejects.toThrow(/owner/); + }); + + it('rejects empty owner', async () => { + (entity as any).spec.owner = ''; + await expect(defaultValidator.check(entity)).rejects.toThrow(/owner/); + }); + + it('accepts missing system', async () => { + delete (entity as any).spec.system; + await expect(defaultValidator.check(entity)).resolves.toBe(true); + }); + + it('rejects wrong system', async () => { + (entity as any).spec.system = 7; + await expect(defaultValidator.check(entity)).rejects.toThrow(/system/); + }); + + it('rejects empty system', async () => { + (entity as any).spec.system = ''; + await expect(defaultValidator.check(entity)).rejects.toThrow(/system/); + }); +}); + +describe('AIResourceV1alpha1 skill validator', () => { + let entity: SkillAIResourceEntityV1alpha1; beforeEach(() => { entity = { @@ -36,82 +133,69 @@ describe('AIResourceV1alpha1Validator', () => { lifecycle: 'production', owner: 'ai-platform-team', system: 'ai-tooling', + disciplines: ['web', 'backend'], + categories: ['framework'], + agents: ['claude-code'], + dependsOn: ['airesource:default/base-coding-standards'], }, }; }); - it('accepts valid data', async () => { - await expect(validator.check(entity)).resolves.toBe(true); + it('accepts valid skill data with all fields', async () => { + await expect(skillValidator.check(entity)).resolves.toBe(true); }); - it('ignores unknown apiVersion', async () => { - (entity as any).apiVersion = 'backstage.io/v1beta0'; - await expect(validator.check(entity)).resolves.toBe(false); + it('accepts skill with only required fields', async () => { + entity.spec = { + type: 'skill', + lifecycle: 'experimental', + owner: 'team-a', + }; + await expect(skillValidator.check(entity)).resolves.toBe(true); }); - it('ignores unknown kind', async () => { - (entity as any).kind = 'Wizard'; - await expect(validator.check(entity)).resolves.toBe(false); - }); - - it('rejects missing type', async () => { - delete (entity as any).spec.type; - await expect(validator.check(entity)).rejects.toThrow(/type/); - }); - - it('rejects wrong type', async () => { - (entity as any).spec.type = 7; - await expect(validator.check(entity)).rejects.toThrow(/type/); - }); - - it('rejects empty type', async () => { - (entity as any).spec.type = ''; - await expect(validator.check(entity)).rejects.toThrow(/type/); + it('rejects non-skill type', async () => { + (entity as any).spec.type = 'rule'; + await expect(skillValidator.check(entity)).rejects.toThrow(/type/); }); it('rejects missing lifecycle', async () => { delete (entity as any).spec.lifecycle; - await expect(validator.check(entity)).rejects.toThrow(/lifecycle/); - }); - - it('rejects wrong lifecycle', async () => { - (entity as any).spec.lifecycle = 7; - await expect(validator.check(entity)).rejects.toThrow(/lifecycle/); - }); - - it('rejects empty lifecycle', async () => { - (entity as any).spec.lifecycle = ''; - await expect(validator.check(entity)).rejects.toThrow(/lifecycle/); + await expect(skillValidator.check(entity)).rejects.toThrow(/lifecycle/); }); it('rejects missing owner', async () => { delete (entity as any).spec.owner; - await expect(validator.check(entity)).rejects.toThrow(/owner/); + await expect(skillValidator.check(entity)).rejects.toThrow(/owner/); }); - it('rejects wrong owner', async () => { - (entity as any).spec.owner = 7; - await expect(validator.check(entity)).rejects.toThrow(/owner/); - }); - - it('rejects empty owner', async () => { - (entity as any).spec.owner = ''; - await expect(validator.check(entity)).rejects.toThrow(/owner/); - }); - - it('accepts missing system', async () => { + it('accepts missing optional fields', async () => { delete (entity as any).spec.system; - await expect(validator.check(entity)).resolves.toBe(true); + delete (entity as any).spec.disciplines; + delete (entity as any).spec.categories; + delete (entity as any).spec.agents; + delete (entity as any).spec.dependsOn; + await expect(skillValidator.check(entity)).resolves.toBe(true); }); - it('rejects wrong system', async () => { - (entity as any).spec.system = 7; - await expect(validator.check(entity)).rejects.toThrow(/system/); + it('rejects disciplines with empty strings', async () => { + (entity as any).spec.disciplines = ['']; + await expect(skillValidator.check(entity)).rejects.toThrow(/disciplines/); }); - it('rejects empty system', async () => { - (entity as any).spec.system = ''; - await expect(validator.check(entity)).rejects.toThrow(/system/); + it('rejects categories with wrong type', async () => { + (entity as any).spec.categories = 'not-an-array'; + await expect(skillValidator.check(entity)).rejects.toThrow(/categories/); + }); + + it('rejects agents with wrong item type', async () => { + (entity as any).spec.agents = [42]; + await expect(skillValidator.check(entity)).rejects.toThrow(/agents/); + }); + + it('rejects dependsOn with empty strings', async () => { + (entity as any).spec.dependsOn = ['']; + await expect(skillValidator.check(entity)).rejects.toThrow(/dependsOn/); }); }); @@ -143,3 +227,35 @@ describe('isAIResourceEntity', () => { expect(isAIResourceEntity(entity)).toBe(false); }); }); + +describe('isSkillAIResourceEntity', () => { + it('returns true for a skill AIResource', () => { + const entity: Entity = { + apiVersion: 'backstage.io/v1alpha1', + kind: 'AIResource', + metadata: { name: 'test' }, + spec: { type: 'skill' }, + }; + expect(isSkillAIResourceEntity(entity)).toBe(true); + }); + + it('returns false for a non-skill AIResource', () => { + const entity: Entity = { + apiVersion: 'backstage.io/v1alpha1', + kind: 'AIResource', + metadata: { name: 'test' }, + spec: { type: 'rule' }, + }; + expect(isSkillAIResourceEntity(entity)).toBe(false); + }); + + it('returns false for a different kind', () => { + const entity: Entity = { + apiVersion: 'backstage.io/v1alpha1', + kind: 'Component', + metadata: { name: 'test' }, + spec: { type: 'skill' }, + }; + expect(isSkillAIResourceEntity(entity)).toBe(false); + }); +}); diff --git a/plugins/catalog-backend-module-ai-resource-entity-model/src/AIResourceEntityV1alpha1.ts b/plugins/catalog-backend-module-ai-resource-entity-model/src/AIResourceEntityV1alpha1.ts index b272ffb02a..269a70d365 100644 --- a/plugins/catalog-backend-module-ai-resource-entity-model/src/AIResourceEntityV1alpha1.ts +++ b/plugins/catalog-backend-module-ai-resource-entity-model/src/AIResourceEntityV1alpha1.ts @@ -21,15 +21,15 @@ import { } from '@backstage/catalog-model'; import { createCatalogModelLayer } from '@backstage/catalog-model/alpha'; import type { JsonObject } from '@backstage/types'; -import jsonSchema from './schema/AIResource.v1alpha1.schema.json'; +import defaultJsonSchema from './schema/AIResource.v1alpha1.schema.json'; +import skillJsonSchema from './schema/AIResource.v1alpha1.skill.schema.json'; /** - * Backstage catalog AIResource kind Entity. Represents contextual information - * consumed by AI coding tools, such as skills and rules. + * Default AIResource entity for types that don't have a structured spec. * * @public */ -export interface AIResourceEntityV1alpha1 extends Entity { +export interface AIResourceEntityV1alpha1Default extends Entity { apiVersion: 'backstage.io/v1alpha1'; kind: 'AIResource'; spec: { @@ -40,16 +40,60 @@ export interface AIResourceEntityV1alpha1 extends Entity { }; } -const validator = entityKindSchemaValidator(jsonSchema); +/** + * AIResource entity with spec.type 'skill'. Represents reusable contextual + * knowledge consumed by AI coding tools. + * + * @public + */ +export interface SkillAIResourceEntityV1alpha1 extends Entity { + apiVersion: 'backstage.io/v1alpha1'; + kind: 'AIResource'; + spec: { + type: 'skill'; + lifecycle: string; + owner: string; + system?: string; + disciplines?: string[]; + categories?: string[]; + agents?: string[]; + dependsOn?: string[]; + }; +} /** - * Entity data validator for {@link AIResourceEntityV1alpha1}. + * Backstage catalog AIResource kind Entity. Represents contextual information + * consumed by AI coding tools, such as skills and rules. + * + * @public + */ +export type AIResourceEntityV1alpha1 = + | AIResourceEntityV1alpha1Default + | SkillAIResourceEntityV1alpha1; + +const defaultValidator = entityKindSchemaValidator(defaultJsonSchema); + +/** + * Entity data validator for the default {@link AIResourceEntityV1alpha1}. * * @public */ export const aiResourceEntityV1alpha1Validator: KindValidator = { async check(data: Entity) { - return validator(data) === data; + return defaultValidator(data) === data; + }, +}; + +const skillValidator = entityKindSchemaValidator(skillJsonSchema); + +/** + * Entity data validator for {@link SkillAIResourceEntityV1alpha1}. + * + * @public + */ +export const skillAIResourceEntityV1alpha1Validator: KindValidator = { + async check(data: Entity) { + return skillValidator(data) === data; }, }; @@ -63,6 +107,16 @@ export const isAIResourceEntity = ( ): entity is AIResourceEntityV1alpha1 => entity.apiVersion === 'backstage.io/v1alpha1' && entity.kind === 'AIResource'; +/** + * Type guard for {@link SkillAIResourceEntityV1alpha1}. + * + * @public + */ +export const isSkillAIResourceEntity = ( + entity: Entity, +): entity is SkillAIResourceEntityV1alpha1 => + isAIResourceEntity(entity) && entity.spec.type === 'skill'; + /** * Extends the catalog model with the AIResource kind. * @@ -99,7 +153,35 @@ export const aiResourceEntityModel = createCatalogModelLayer({ }, ], schema: { - jsonSchema: jsonSchema as JsonObject, + jsonSchema: defaultJsonSchema as JsonObject, + }, + }, + { + name: 'v1alpha1', + specType: 'skill', + relationFields: [ + { + selector: { path: 'spec.owner' }, + relation: 'ownedBy', + defaultKind: 'Group', + defaultNamespace: 'inherit', + allowedKinds: ['Group', 'User'], + }, + { + selector: { path: 'spec.system' }, + relation: 'partOf', + defaultKind: 'System', + defaultNamespace: 'inherit', + }, + { + selector: { path: 'spec.dependsOn' }, + relation: 'dependsOn', + defaultKind: 'AIResource', + defaultNamespace: 'inherit', + }, + ], + schema: { + jsonSchema: skillJsonSchema as JsonObject, }, }, ], diff --git a/plugins/catalog-backend-module-ai-resource-entity-model/src/index.ts b/plugins/catalog-backend-module-ai-resource-entity-model/src/index.ts index cf1b636db1..afd2bc82d2 100644 --- a/plugins/catalog-backend-module-ai-resource-entity-model/src/index.ts +++ b/plugins/catalog-backend-module-ai-resource-entity-model/src/index.ts @@ -20,9 +20,15 @@ * @packageDocumentation */ -export type { AIResourceEntityV1alpha1 } from './AIResourceEntityV1alpha1'; +export type { + AIResourceEntityV1alpha1, + AIResourceEntityV1alpha1Default, + SkillAIResourceEntityV1alpha1, +} from './AIResourceEntityV1alpha1'; export { aiResourceEntityV1alpha1Validator, + skillAIResourceEntityV1alpha1Validator, isAIResourceEntity, + isSkillAIResourceEntity, } from './AIResourceEntityV1alpha1'; export { catalogModuleAIResourceEntityModel as default } from './module'; diff --git a/plugins/catalog-backend-module-ai-resource-entity-model/src/module.ts b/plugins/catalog-backend-module-ai-resource-entity-model/src/module.ts index 6ccab581df..d0c00d810d 100644 --- a/plugins/catalog-backend-module-ai-resource-entity-model/src/module.ts +++ b/plugins/catalog-backend-module-ai-resource-entity-model/src/module.ts @@ -15,7 +15,6 @@ */ import { createBackendModule } from '@backstage/backend-plugin-api'; -import { CatalogModelSources } from '@backstage/catalog-model/alpha'; import { catalogModelExtensionPoint } from '@backstage/plugin-catalog-node/alpha'; import { aiResourceEntityModel } from './AIResourceEntityV1alpha1'; @@ -33,9 +32,11 @@ export const catalogModuleAIResourceEntityModel = createBackendModule({ model: catalogModelExtensionPoint, }, async init({ model }) { - model.addModelSource( - CatalogModelSources.static([aiResourceEntityModel]), - ); + model.addModelSource({ + async *read() { + yield { data: [{ layer: aiResourceEntityModel }] }; + }, + }); }, }); }, diff --git a/plugins/catalog-backend-module-ai-resource-entity-model/src/schema/AIResource.v1alpha1.skill.schema.json b/plugins/catalog-backend-module-ai-resource-entity-model/src/schema/AIResource.v1alpha1.skill.schema.json new file mode 100644 index 0000000000..32f220582c --- /dev/null +++ b/plugins/catalog-backend-module-ai-resource-entity-model/src/schema/AIResource.v1alpha1.skill.schema.json @@ -0,0 +1,101 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema", + "$id": "AIResourceV1alpha1Skill", + "description": "An AI resource of type 'skill', representing reusable contextual knowledge consumed by AI coding tools.", + "examples": [ + { + "apiVersion": "backstage.io/v1alpha1", + "kind": "AIResource", + "metadata": { + "name": "frontend-design", + "description": "Skill for creating production-grade frontend interfaces" + }, + "spec": { + "type": "skill", + "lifecycle": "production", + "owner": "ai-platform-team", + "system": "ai-tooling", + "disciplines": ["web"], + "categories": ["framework"], + "agents": ["claude-code"] + } + } + ], + "allOf": [ + { + "$ref": "Entity" + }, + { + "type": "object", + "required": ["spec"], + "properties": { + "apiVersion": { + "enum": ["backstage.io/v1alpha1"] + }, + "kind": { + "enum": ["AIResource"] + }, + "spec": { + "type": "object", + "required": ["type", "lifecycle", "owner"], + "properties": { + "type": { + "type": "string", + "enum": ["skill"], + "description": "The type of AI resource." + }, + "lifecycle": { + "type": "string", + "description": "The lifecycle state of the AI resource.", + "examples": ["experimental", "production", "deprecated"], + "minLength": 1 + }, + "owner": { + "type": "string", + "description": "An entity reference to the owner of the AI resource.", + "examples": ["ai-platform-team", "user:john.johnson"], + "minLength": 1 + }, + "system": { + "type": "string", + "description": "An entity reference to the system that the AI resource belongs to.", + "minLength": 1 + }, + "disciplines": { + "type": "array", + "description": "Engineering disciplines this skill applies to.", + "items": { + "type": "string", + "minLength": 1 + } + }, + "categories": { + "type": "array", + "description": "Descriptive categories for the skill.", + "items": { + "type": "string", + "minLength": 1 + } + }, + "agents": { + "type": "array", + "description": "AI coding tools this skill is designed for.", + "items": { + "type": "string", + "minLength": 1 + } + }, + "dependsOn": { + "type": "array", + "description": "Entity references to other AI resources this skill depends on.", + "items": { + "type": "string", + "minLength": 1 + } + } + } + } + } + } + ] +} From 37267be659105e4506cff92de48ffd77e3c648b8 Mon Sep 17 00:00:00 2001 From: benjdlambert Date: Fri, 15 May 2026 14:24:02 +0200 Subject: [PATCH 03/15] fix: clarify type guard test names Signed-off-by: benjdlambert --- .../src/AIResourceEntityV1alpha1.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/catalog-backend-module-ai-resource-entity-model/src/AIResourceEntityV1alpha1.test.ts b/plugins/catalog-backend-module-ai-resource-entity-model/src/AIResourceEntityV1alpha1.test.ts index 8acbc5bc4a..d821a1f3d5 100644 --- a/plugins/catalog-backend-module-ai-resource-entity-model/src/AIResourceEntityV1alpha1.test.ts +++ b/plugins/catalog-backend-module-ai-resource-entity-model/src/AIResourceEntityV1alpha1.test.ts @@ -200,7 +200,7 @@ describe('AIResourceV1alpha1 skill validator', () => { }); describe('isAIResourceEntity', () => { - it('returns true for a valid AIResource entity', () => { + it('returns true when apiVersion and kind match', () => { const entity: Entity = { apiVersion: 'backstage.io/v1alpha1', kind: 'AIResource', From c41b6d09f713f43c0bbc2d0c37bce0ab59135404 Mon Sep 17 00:00:00 2001 From: benjdlambert Date: Fri, 15 May 2026 14:24:32 +0200 Subject: [PATCH 04/15] docs: add README for ai-resource-entity-model module Signed-off-by: benjdlambert --- .../README.md | 42 +++++++++++++++++++ 1 file changed, 42 insertions(+) create mode 100644 plugins/catalog-backend-module-ai-resource-entity-model/README.md diff --git a/plugins/catalog-backend-module-ai-resource-entity-model/README.md b/plugins/catalog-backend-module-ai-resource-entity-model/README.md new file mode 100644 index 0000000000..c30fb20941 --- /dev/null +++ b/plugins/catalog-backend-module-ai-resource-entity-model/README.md @@ -0,0 +1,42 @@ +# @backstage/plugin-catalog-backend-module-ai-resource-entity-model + +Adds support for the `AIResource` entity kind to the catalog backend plugin. AI resources represent contextual information consumed by AI coding tools, such as skills and rules. + +## Installation + +Add the module to your backend: + +```ts +backend.add( + import('@backstage/plugin-catalog-backend-module-ai-resource-entity-model'), +); +``` + +## Entity shape + +```yaml +apiVersion: backstage.io/v1alpha1 +kind: AIResource +metadata: + name: frontend-design + description: Skill for creating production-grade frontend interfaces +spec: + type: skill + lifecycle: production + owner: ai-platform-team + system: ai-tooling + disciplines: + - web + categories: + - framework + agents: + - claude-code + dependsOn: + - airesource:default/base-coding-standards +``` + +The `type` field determines which spec fields are available. Currently supported types: + +- **`skill`** — reusable contextual knowledge for AI coding tools. Supports additional fields: `disciplines`, `categories`, `agents`, `dependsOn`. + +Any other `type` value is accepted with the base spec fields: `type`, `lifecycle`, `owner`, and optionally `system`. From 65c8533ba18d69d31a5fe0819d8d8ad608854007 Mon Sep 17 00:00:00 2001 From: benjdlambert Date: Fri, 15 May 2026 15:56:08 +0200 Subject: [PATCH 05/15] refactor: extract shared relation fields and fix type guard safety Signed-off-by: benjdlambert --- .../src/AIResourceEntityV1alpha1.ts | 55 +++++++++---------- 1 file changed, 25 insertions(+), 30 deletions(-) diff --git a/plugins/catalog-backend-module-ai-resource-entity-model/src/AIResourceEntityV1alpha1.ts b/plugins/catalog-backend-module-ai-resource-entity-model/src/AIResourceEntityV1alpha1.ts index 269a70d365..2cd3907dc7 100644 --- a/plugins/catalog-backend-module-ai-resource-entity-model/src/AIResourceEntityV1alpha1.ts +++ b/plugins/catalog-backend-module-ai-resource-entity-model/src/AIResourceEntityV1alpha1.ts @@ -115,7 +115,28 @@ export const isAIResourceEntity = ( export const isSkillAIResourceEntity = ( entity: Entity, ): entity is SkillAIResourceEntityV1alpha1 => - isAIResourceEntity(entity) && entity.spec.type === 'skill'; + isAIResourceEntity(entity) && entity.spec?.type === 'skill'; + +/** + * Extends the catalog model with the AIResource kind. + * + * @alpha + */ +const baseRelationFields = [ + { + selector: { path: 'spec.owner' }, + relation: 'ownedBy', + defaultKind: 'Group', + defaultNamespace: 'inherit' as const, + allowedKinds: ['Group', 'User'], + }, + { + selector: { path: 'spec.system' }, + relation: 'partOf', + defaultKind: 'System', + defaultNamespace: 'inherit' as const, + }, +]; /** * Extends the catalog model with the AIResource kind. @@ -137,21 +158,7 @@ export const aiResourceEntityModel = createCatalogModelLayer({ versions: [ { name: 'v1alpha1', - relationFields: [ - { - selector: { path: 'spec.owner' }, - relation: 'ownedBy', - defaultKind: 'Group', - defaultNamespace: 'inherit', - allowedKinds: ['Group', 'User'], - }, - { - selector: { path: 'spec.system' }, - relation: 'partOf', - defaultKind: 'System', - defaultNamespace: 'inherit', - }, - ], + relationFields: baseRelationFields, schema: { jsonSchema: defaultJsonSchema as JsonObject, }, @@ -160,24 +167,12 @@ export const aiResourceEntityModel = createCatalogModelLayer({ name: 'v1alpha1', specType: 'skill', relationFields: [ - { - selector: { path: 'spec.owner' }, - relation: 'ownedBy', - defaultKind: 'Group', - defaultNamespace: 'inherit', - allowedKinds: ['Group', 'User'], - }, - { - selector: { path: 'spec.system' }, - relation: 'partOf', - defaultKind: 'System', - defaultNamespace: 'inherit', - }, + ...baseRelationFields, { selector: { path: 'spec.dependsOn' }, relation: 'dependsOn', defaultKind: 'AIResource', - defaultNamespace: 'inherit', + defaultNamespace: 'inherit' as const, }, ], schema: { From 8e1e1bb06eaa77d2b09ec3866f29ba093fae3598 Mon Sep 17 00:00:00 2001 From: benjdlambert Date: Mon, 18 May 2026 10:37:26 +0200 Subject: [PATCH 06/15] fix: use CatalogModelSources.static for model registration Signed-off-by: benjdlambert --- .../src/AIResourceEntityV1alpha1.ts | 11 +++-------- .../src/module.ts | 9 ++++----- 2 files changed, 7 insertions(+), 13 deletions(-) diff --git a/plugins/catalog-backend-module-ai-resource-entity-model/src/AIResourceEntityV1alpha1.ts b/plugins/catalog-backend-module-ai-resource-entity-model/src/AIResourceEntityV1alpha1.ts index 2cd3907dc7..3623c75d43 100644 --- a/plugins/catalog-backend-module-ai-resource-entity-model/src/AIResourceEntityV1alpha1.ts +++ b/plugins/catalog-backend-module-ai-resource-entity-model/src/AIResourceEntityV1alpha1.ts @@ -117,24 +117,19 @@ export const isSkillAIResourceEntity = ( ): entity is SkillAIResourceEntityV1alpha1 => isAIResourceEntity(entity) && entity.spec?.type === 'skill'; -/** - * Extends the catalog model with the AIResource kind. - * - * @alpha - */ const baseRelationFields = [ { selector: { path: 'spec.owner' }, relation: 'ownedBy', defaultKind: 'Group', - defaultNamespace: 'inherit' as const, + defaultNamespace: 'inherit', allowedKinds: ['Group', 'User'], }, { selector: { path: 'spec.system' }, relation: 'partOf', defaultKind: 'System', - defaultNamespace: 'inherit' as const, + defaultNamespace: 'inherit', }, ]; @@ -172,7 +167,7 @@ export const aiResourceEntityModel = createCatalogModelLayer({ selector: { path: 'spec.dependsOn' }, relation: 'dependsOn', defaultKind: 'AIResource', - defaultNamespace: 'inherit' as const, + defaultNamespace: 'inherit', }, ], schema: { diff --git a/plugins/catalog-backend-module-ai-resource-entity-model/src/module.ts b/plugins/catalog-backend-module-ai-resource-entity-model/src/module.ts index d0c00d810d..6ccab581df 100644 --- a/plugins/catalog-backend-module-ai-resource-entity-model/src/module.ts +++ b/plugins/catalog-backend-module-ai-resource-entity-model/src/module.ts @@ -15,6 +15,7 @@ */ import { createBackendModule } from '@backstage/backend-plugin-api'; +import { CatalogModelSources } from '@backstage/catalog-model/alpha'; import { catalogModelExtensionPoint } from '@backstage/plugin-catalog-node/alpha'; import { aiResourceEntityModel } from './AIResourceEntityV1alpha1'; @@ -32,11 +33,9 @@ export const catalogModuleAIResourceEntityModel = createBackendModule({ model: catalogModelExtensionPoint, }, async init({ model }) { - model.addModelSource({ - async *read() { - yield { data: [{ layer: aiResourceEntityModel }] }; - }, - }); + model.addModelSource( + CatalogModelSources.static([aiResourceEntityModel]), + ); }, }); }, From 07a2ece6714c8b6346fd0efac08317c3c4fde30c Mon Sep 17 00:00:00 2001 From: benjdlambert Date: Mon, 18 May 2026 14:16:02 +0200 Subject: [PATCH 07/15] feat: install AIResource module in example backend with test entities Signed-off-by: benjdlambert --- app-config.yaml | 1 + packages/backend/package.json | 1 + packages/backend/src/index.ts | 3 +++ .../ai-resources/frontend-design-skill.yaml | 18 ++++++++++++++++++ .../ai-resources/use-internal-apis-rule.yaml | 10 ++++++++++ .../examples/all-ai-resources.yaml | 9 +++++++++ packages/catalog-model/examples/all.yaml | 1 + yarn.lock | 3 ++- 8 files changed, 45 insertions(+), 1 deletion(-) create mode 100644 packages/catalog-model/examples/ai-resources/frontend-design-skill.yaml create mode 100644 packages/catalog-model/examples/ai-resources/use-internal-apis-rule.yaml create mode 100644 packages/catalog-model/examples/all-ai-resources.yaml diff --git a/app-config.yaml b/app-config.yaml index 11f812bb3d..f586539fd5 100644 --- a/app-config.yaml +++ b/app-config.yaml @@ -287,6 +287,7 @@ catalog: pullRequestBranchName: backstage-integration rules: - allow: + - AIResource - Component - API - Resource diff --git a/packages/backend/package.json b/packages/backend/package.json index a7fc1b37e9..1ebdb39406 100644 --- a/packages/backend/package.json +++ b/packages/backend/package.json @@ -40,6 +40,7 @@ "@backstage/plugin-auth-backend-module-openshift-provider": "workspace:^", "@backstage/plugin-auth-node": "workspace:^", "@backstage/plugin-catalog-backend": "workspace:^", + "@backstage/plugin-catalog-backend-module-ai-resource-entity-model": "workspace:^", "@backstage/plugin-catalog-backend-module-backstage-openapi": "workspace:^", "@backstage/plugin-catalog-backend-module-logs": "workspace:^", "@backstage/plugin-catalog-backend-module-openapi": "workspace:^", diff --git a/packages/backend/src/index.ts b/packages/backend/src/index.ts index e77c83baac..9a74cf9f51 100644 --- a/packages/backend/src/index.ts +++ b/packages/backend/src/index.ts @@ -46,6 +46,9 @@ backend.add(import('@backstage/plugin-auth-backend-module-guest-provider')); backend.add(import('@backstage/plugin-auth-backend-module-openshift-provider')); backend.add(import('@backstage/plugin-app-backend')); backend.add(import('@backstage/plugin-catalog-backend-module-unprocessed')); +backend.add( + import('@backstage/plugin-catalog-backend-module-ai-resource-entity-model'), +); backend.add( import('@backstage/plugin-catalog-backend-module-scaffolder-entity-model'), ); diff --git a/packages/catalog-model/examples/ai-resources/frontend-design-skill.yaml b/packages/catalog-model/examples/ai-resources/frontend-design-skill.yaml new file mode 100644 index 0000000000..bbe5f049c9 --- /dev/null +++ b/packages/catalog-model/examples/ai-resources/frontend-design-skill.yaml @@ -0,0 +1,18 @@ +apiVersion: backstage.io/v1alpha1 +kind: AIResource +metadata: + name: frontend-design + description: Skill for creating production-grade frontend interfaces using the internal design system +spec: + type: skill + lifecycle: production + owner: team-a + system: artist-engagement-portal + disciplines: + - web + categories: + - framework + - design-system + agents: + - claude-code + - copilot diff --git a/packages/catalog-model/examples/ai-resources/use-internal-apis-rule.yaml b/packages/catalog-model/examples/ai-resources/use-internal-apis-rule.yaml new file mode 100644 index 0000000000..01050e95af --- /dev/null +++ b/packages/catalog-model/examples/ai-resources/use-internal-apis-rule.yaml @@ -0,0 +1,10 @@ +apiVersion: backstage.io/v1alpha1 +kind: AIResource +metadata: + name: use-internal-apis + description: Agents must use internal API clients instead of raw HTTP calls +spec: + type: rule + lifecycle: production + owner: team-a + system: artist-engagement-portal diff --git a/packages/catalog-model/examples/all-ai-resources.yaml b/packages/catalog-model/examples/all-ai-resources.yaml new file mode 100644 index 0000000000..afa70f821a --- /dev/null +++ b/packages/catalog-model/examples/all-ai-resources.yaml @@ -0,0 +1,9 @@ +apiVersion: backstage.io/v1alpha1 +kind: Location +metadata: + name: example-ai-resources + description: A collection of all Backstage example AI resources +spec: + targets: + - ./ai-resources/frontend-design-skill.yaml + - ./ai-resources/use-internal-apis-rule.yaml diff --git a/packages/catalog-model/examples/all.yaml b/packages/catalog-model/examples/all.yaml index c8e073345c..9512a39b4f 100644 --- a/packages/catalog-model/examples/all.yaml +++ b/packages/catalog-model/examples/all.yaml @@ -5,6 +5,7 @@ metadata: description: A collection of all Backstage example entities, except users, groups, and templates spec: targets: + - ./all-ai-resources.yaml - ./all-apis.yaml - ./all-components.yaml - ./all-domains.yaml diff --git a/yarn.lock b/yarn.lock index 2737bf4ac8..401c4072d8 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4759,7 +4759,7 @@ __metadata: languageName: unknown linkType: soft -"@backstage/plugin-catalog-backend-module-ai-resource-entity-model@workspace:plugins/catalog-backend-module-ai-resource-entity-model": +"@backstage/plugin-catalog-backend-module-ai-resource-entity-model@workspace:^, @backstage/plugin-catalog-backend-module-ai-resource-entity-model@workspace:plugins/catalog-backend-module-ai-resource-entity-model": version: 0.0.0-use.local resolution: "@backstage/plugin-catalog-backend-module-ai-resource-entity-model@workspace:plugins/catalog-backend-module-ai-resource-entity-model" dependencies: @@ -29306,6 +29306,7 @@ __metadata: "@backstage/plugin-auth-backend-module-openshift-provider": "workspace:^" "@backstage/plugin-auth-node": "workspace:^" "@backstage/plugin-catalog-backend": "workspace:^" + "@backstage/plugin-catalog-backend-module-ai-resource-entity-model": "workspace:^" "@backstage/plugin-catalog-backend-module-backstage-openapi": "workspace:^" "@backstage/plugin-catalog-backend-module-logs": "workspace:^" "@backstage/plugin-catalog-backend-module-openapi": "workspace:^" From cf297369233f23447dfc343a1e79471159e61545 Mon Sep 17 00:00:00 2001 From: benjdlambert Date: Mon, 18 May 2026 16:04:51 +0200 Subject: [PATCH 08/15] refactor: rename AIResource to AiResource for PascalCase consistency Signed-off-by: benjdlambert --- .changeset/add-ai-resource-entity-model.md | 2 +- app-config.yaml | 2 +- .../ai-resources/frontend-design-skill.yaml | 2 +- .../ai-resources/use-internal-apis-rule.yaml | 2 +- .../README.md | 4 +- .../report.api.md | 28 ++++----- ...st.ts => AiResourceEntityV1alpha1.test.ts} | 52 ++++++++--------- ...1alpha1.ts => AiResourceEntityV1alpha1.ts} | 58 +++++++++---------- .../src/alpha.ts | 2 +- .../src/index.ts | 20 +++---- .../src/module.test.ts | 6 +- .../src/module.ts | 6 +- ...a.json => AiResource.v1alpha1.schema.json} | 6 +- ... => AiResource.v1alpha1.skill.schema.json} | 6 +- 14 files changed, 98 insertions(+), 98 deletions(-) rename plugins/catalog-backend-module-ai-resource-entity-model/src/{AIResourceEntityV1alpha1.test.ts => AiResourceEntityV1alpha1.test.ts} (86%) rename plugins/catalog-backend-module-ai-resource-entity-model/src/{AIResourceEntityV1alpha1.ts => AiResourceEntityV1alpha1.ts} (70%) rename plugins/catalog-backend-module-ai-resource-entity-model/src/schema/{AIResource.v1alpha1.schema.json => AiResource.v1alpha1.schema.json} (95%) rename plugins/catalog-backend-module-ai-resource-entity-model/src/schema/{AIResource.v1alpha1.skill.schema.json => AiResource.v1alpha1.skill.schema.json} (96%) diff --git a/.changeset/add-ai-resource-entity-model.md b/.changeset/add-ai-resource-entity-model.md index 9a1d853471..97e6cb9d52 100644 --- a/.changeset/add-ai-resource-entity-model.md +++ b/.changeset/add-ai-resource-entity-model.md @@ -2,4 +2,4 @@ '@backstage/plugin-catalog-backend-module-ai-resource-entity-model': minor --- -Introduced the `AIResource` catalog entity kind as an opt-in backend module. Install this module to register support for `AIResource` entities in your catalog, used to represent contextual information consumed by AI coding tools such as skills and rules. +Introduced the `AiResource` catalog entity kind as an opt-in backend module. Install this module to register support for `AiResource` entities in your catalog, used to represent contextual information consumed by AI coding tools such as skills and rules. diff --git a/app-config.yaml b/app-config.yaml index f586539fd5..7b3f748cb5 100644 --- a/app-config.yaml +++ b/app-config.yaml @@ -287,7 +287,7 @@ catalog: pullRequestBranchName: backstage-integration rules: - allow: - - AIResource + - AiResource - Component - API - Resource diff --git a/packages/catalog-model/examples/ai-resources/frontend-design-skill.yaml b/packages/catalog-model/examples/ai-resources/frontend-design-skill.yaml index bbe5f049c9..9621733471 100644 --- a/packages/catalog-model/examples/ai-resources/frontend-design-skill.yaml +++ b/packages/catalog-model/examples/ai-resources/frontend-design-skill.yaml @@ -1,5 +1,5 @@ apiVersion: backstage.io/v1alpha1 -kind: AIResource +kind: AiResource metadata: name: frontend-design description: Skill for creating production-grade frontend interfaces using the internal design system diff --git a/packages/catalog-model/examples/ai-resources/use-internal-apis-rule.yaml b/packages/catalog-model/examples/ai-resources/use-internal-apis-rule.yaml index 01050e95af..bd06d5bb22 100644 --- a/packages/catalog-model/examples/ai-resources/use-internal-apis-rule.yaml +++ b/packages/catalog-model/examples/ai-resources/use-internal-apis-rule.yaml @@ -1,5 +1,5 @@ apiVersion: backstage.io/v1alpha1 -kind: AIResource +kind: AiResource metadata: name: use-internal-apis description: Agents must use internal API clients instead of raw HTTP calls diff --git a/plugins/catalog-backend-module-ai-resource-entity-model/README.md b/plugins/catalog-backend-module-ai-resource-entity-model/README.md index c30fb20941..7d229dd00a 100644 --- a/plugins/catalog-backend-module-ai-resource-entity-model/README.md +++ b/plugins/catalog-backend-module-ai-resource-entity-model/README.md @@ -1,6 +1,6 @@ # @backstage/plugin-catalog-backend-module-ai-resource-entity-model -Adds support for the `AIResource` entity kind to the catalog backend plugin. AI resources represent contextual information consumed by AI coding tools, such as skills and rules. +Adds support for the `AiResource` entity kind to the catalog backend plugin. AI resources represent contextual information consumed by AI coding tools, such as skills and rules. ## Installation @@ -16,7 +16,7 @@ backend.add( ```yaml apiVersion: backstage.io/v1alpha1 -kind: AIResource +kind: AiResource metadata: name: frontend-design description: Skill for creating production-grade frontend interfaces diff --git a/plugins/catalog-backend-module-ai-resource-entity-model/report.api.md b/plugins/catalog-backend-module-ai-resource-entity-model/report.api.md index 6e62498abe..ad21f4982f 100644 --- a/plugins/catalog-backend-module-ai-resource-entity-model/report.api.md +++ b/plugins/catalog-backend-module-ai-resource-entity-model/report.api.md @@ -8,16 +8,16 @@ import { Entity } from '@backstage/catalog-model'; import { KindValidator } from '@backstage/catalog-model'; // @public -export type AIResourceEntityV1alpha1 = - | AIResourceEntityV1alpha1Default - | SkillAIResourceEntityV1alpha1; +export type AiResourceEntityV1alpha1 = + | AiResourceEntityV1alpha1Default + | SkillAiResourceEntityV1alpha1; // @public -export interface AIResourceEntityV1alpha1Default extends Entity { +export interface AiResourceEntityV1alpha1Default extends Entity { // (undocumented) apiVersion: 'backstage.io/v1alpha1'; // (undocumented) - kind: 'AIResource'; + kind: 'AiResource'; // (undocumented) spec: { type: string; @@ -31,25 +31,25 @@ export interface AIResourceEntityV1alpha1Default extends Entity { export const aiResourceEntityV1alpha1Validator: KindValidator; // @public -const catalogModuleAIResourceEntityModel: BackendFeature; -export default catalogModuleAIResourceEntityModel; +const catalogModuleAiResourceEntityModel: BackendFeature; +export default catalogModuleAiResourceEntityModel; // @public -export const isAIResourceEntity: ( +export const isAiResourceEntity: ( entity: Entity, -) => entity is AIResourceEntityV1alpha1; +) => entity is AiResourceEntityV1alpha1; // @public -export const isSkillAIResourceEntity: ( +export const isSkillAiResourceEntity: ( entity: Entity, -) => entity is SkillAIResourceEntityV1alpha1; +) => entity is SkillAiResourceEntityV1alpha1; // @public -export interface SkillAIResourceEntityV1alpha1 extends Entity { +export interface SkillAiResourceEntityV1alpha1 extends Entity { // (undocumented) apiVersion: 'backstage.io/v1alpha1'; // (undocumented) - kind: 'AIResource'; + kind: 'AiResource'; // (undocumented) spec: { type: 'skill'; @@ -64,5 +64,5 @@ export interface SkillAIResourceEntityV1alpha1 extends Entity { } // @public -export const skillAIResourceEntityV1alpha1Validator: KindValidator; +export const skillAiResourceEntityV1alpha1Validator: KindValidator; ``` diff --git a/plugins/catalog-backend-module-ai-resource-entity-model/src/AIResourceEntityV1alpha1.test.ts b/plugins/catalog-backend-module-ai-resource-entity-model/src/AiResourceEntityV1alpha1.test.ts similarity index 86% rename from plugins/catalog-backend-module-ai-resource-entity-model/src/AIResourceEntityV1alpha1.test.ts rename to plugins/catalog-backend-module-ai-resource-entity-model/src/AiResourceEntityV1alpha1.test.ts index d821a1f3d5..308dc1517a 100644 --- a/plugins/catalog-backend-module-ai-resource-entity-model/src/AIResourceEntityV1alpha1.test.ts +++ b/plugins/catalog-backend-module-ai-resource-entity-model/src/AiResourceEntityV1alpha1.test.ts @@ -16,21 +16,21 @@ import type { Entity } from '@backstage/catalog-model'; import { - type AIResourceEntityV1alpha1Default, - type SkillAIResourceEntityV1alpha1, + type AiResourceEntityV1alpha1Default, + type SkillAiResourceEntityV1alpha1, aiResourceEntityV1alpha1Validator as defaultValidator, - skillAIResourceEntityV1alpha1Validator as skillValidator, - isAIResourceEntity, - isSkillAIResourceEntity, -} from './AIResourceEntityV1alpha1'; + skillAiResourceEntityV1alpha1Validator as skillValidator, + isAiResourceEntity, + isSkillAiResourceEntity, +} from './AiResourceEntityV1alpha1'; -describe('AIResourceV1alpha1 default validator', () => { - let entity: AIResourceEntityV1alpha1Default; +describe('AiResourceV1alpha1 default validator', () => { + let entity: AiResourceEntityV1alpha1Default; beforeEach(() => { entity = { apiVersion: 'backstage.io/v1alpha1', - kind: 'AIResource', + kind: 'AiResource', metadata: { name: 'internal-design-system', }, @@ -118,13 +118,13 @@ describe('AIResourceV1alpha1 default validator', () => { }); }); -describe('AIResourceV1alpha1 skill validator', () => { - let entity: SkillAIResourceEntityV1alpha1; +describe('AiResourceV1alpha1 skill validator', () => { + let entity: SkillAiResourceEntityV1alpha1; beforeEach(() => { entity = { apiVersion: 'backstage.io/v1alpha1', - kind: 'AIResource', + kind: 'AiResource', metadata: { name: 'frontend-design', }, @@ -199,14 +199,14 @@ describe('AIResourceV1alpha1 skill validator', () => { }); }); -describe('isAIResourceEntity', () => { +describe('isAiResourceEntity', () => { it('returns true when apiVersion and kind match', () => { const entity: Entity = { apiVersion: 'backstage.io/v1alpha1', - kind: 'AIResource', + kind: 'AiResource', metadata: { name: 'test' }, }; - expect(isAIResourceEntity(entity)).toBe(true); + expect(isAiResourceEntity(entity)).toBe(true); }); it('returns false for a different kind', () => { @@ -215,38 +215,38 @@ describe('isAIResourceEntity', () => { kind: 'Component', metadata: { name: 'test' }, }; - expect(isAIResourceEntity(entity)).toBe(false); + expect(isAiResourceEntity(entity)).toBe(false); }); it('returns false for a different apiVersion', () => { const entity: Entity = { apiVersion: 'backstage.io/v1beta1', - kind: 'AIResource', + kind: 'AiResource', metadata: { name: 'test' }, }; - expect(isAIResourceEntity(entity)).toBe(false); + expect(isAiResourceEntity(entity)).toBe(false); }); }); -describe('isSkillAIResourceEntity', () => { - it('returns true for a skill AIResource', () => { +describe('isSkillAiResourceEntity', () => { + it('returns true for a skill AiResource', () => { const entity: Entity = { apiVersion: 'backstage.io/v1alpha1', - kind: 'AIResource', + kind: 'AiResource', metadata: { name: 'test' }, spec: { type: 'skill' }, }; - expect(isSkillAIResourceEntity(entity)).toBe(true); + expect(isSkillAiResourceEntity(entity)).toBe(true); }); - it('returns false for a non-skill AIResource', () => { + it('returns false for a non-skill AiResource', () => { const entity: Entity = { apiVersion: 'backstage.io/v1alpha1', - kind: 'AIResource', + kind: 'AiResource', metadata: { name: 'test' }, spec: { type: 'rule' }, }; - expect(isSkillAIResourceEntity(entity)).toBe(false); + expect(isSkillAiResourceEntity(entity)).toBe(false); }); it('returns false for a different kind', () => { @@ -256,6 +256,6 @@ describe('isSkillAIResourceEntity', () => { metadata: { name: 'test' }, spec: { type: 'skill' }, }; - expect(isSkillAIResourceEntity(entity)).toBe(false); + expect(isSkillAiResourceEntity(entity)).toBe(false); }); }); diff --git a/plugins/catalog-backend-module-ai-resource-entity-model/src/AIResourceEntityV1alpha1.ts b/plugins/catalog-backend-module-ai-resource-entity-model/src/AiResourceEntityV1alpha1.ts similarity index 70% rename from plugins/catalog-backend-module-ai-resource-entity-model/src/AIResourceEntityV1alpha1.ts rename to plugins/catalog-backend-module-ai-resource-entity-model/src/AiResourceEntityV1alpha1.ts index 3623c75d43..dac2c3c421 100644 --- a/plugins/catalog-backend-module-ai-resource-entity-model/src/AIResourceEntityV1alpha1.ts +++ b/plugins/catalog-backend-module-ai-resource-entity-model/src/AiResourceEntityV1alpha1.ts @@ -21,17 +21,17 @@ import { } from '@backstage/catalog-model'; import { createCatalogModelLayer } from '@backstage/catalog-model/alpha'; import type { JsonObject } from '@backstage/types'; -import defaultJsonSchema from './schema/AIResource.v1alpha1.schema.json'; -import skillJsonSchema from './schema/AIResource.v1alpha1.skill.schema.json'; +import defaultJsonSchema from './schema/AiResource.v1alpha1.schema.json'; +import skillJsonSchema from './schema/AiResource.v1alpha1.skill.schema.json'; /** - * Default AIResource entity for types that don't have a structured spec. + * Default AiResource entity for types that don't have a structured spec. * * @public */ -export interface AIResourceEntityV1alpha1Default extends Entity { +export interface AiResourceEntityV1alpha1Default extends Entity { apiVersion: 'backstage.io/v1alpha1'; - kind: 'AIResource'; + kind: 'AiResource'; spec: { type: string; lifecycle: string; @@ -41,14 +41,14 @@ export interface AIResourceEntityV1alpha1Default extends Entity { } /** - * AIResource entity with spec.type 'skill'. Represents reusable contextual + * AiResource entity with spec.type 'skill'. Represents reusable contextual * knowledge consumed by AI coding tools. * * @public */ -export interface SkillAIResourceEntityV1alpha1 extends Entity { +export interface SkillAiResourceEntityV1alpha1 extends Entity { apiVersion: 'backstage.io/v1alpha1'; - kind: 'AIResource'; + kind: 'AiResource'; spec: { type: 'skill'; lifecycle: string; @@ -62,19 +62,19 @@ export interface SkillAIResourceEntityV1alpha1 extends Entity { } /** - * Backstage catalog AIResource kind Entity. Represents contextual information + * Backstage catalog AiResource kind Entity. Represents contextual information * consumed by AI coding tools, such as skills and rules. * * @public */ -export type AIResourceEntityV1alpha1 = - | AIResourceEntityV1alpha1Default - | SkillAIResourceEntityV1alpha1; +export type AiResourceEntityV1alpha1 = + | AiResourceEntityV1alpha1Default + | SkillAiResourceEntityV1alpha1; const defaultValidator = entityKindSchemaValidator(defaultJsonSchema); /** - * Entity data validator for the default {@link AIResourceEntityV1alpha1}. + * Entity data validator for the default {@link AiResourceEntityV1alpha1}. * * @public */ @@ -87,54 +87,54 @@ export const aiResourceEntityV1alpha1Validator: KindValidator = { const skillValidator = entityKindSchemaValidator(skillJsonSchema); /** - * Entity data validator for {@link SkillAIResourceEntityV1alpha1}. + * Entity data validator for {@link SkillAiResourceEntityV1alpha1}. * * @public */ -export const skillAIResourceEntityV1alpha1Validator: KindValidator = { +export const skillAiResourceEntityV1alpha1Validator: KindValidator = { async check(data: Entity) { return skillValidator(data) === data; }, }; /** - * Type guard for {@link AIResourceEntityV1alpha1}. + * Type guard for {@link AiResourceEntityV1alpha1}. * * @public */ -export const isAIResourceEntity = ( +export const isAiResourceEntity = ( entity: Entity, -): entity is AIResourceEntityV1alpha1 => - entity.apiVersion === 'backstage.io/v1alpha1' && entity.kind === 'AIResource'; +): entity is AiResourceEntityV1alpha1 => + entity.apiVersion === 'backstage.io/v1alpha1' && entity.kind === 'AiResource'; /** - * Type guard for {@link SkillAIResourceEntityV1alpha1}. + * Type guard for {@link SkillAiResourceEntityV1alpha1}. * * @public */ -export const isSkillAIResourceEntity = ( +export const isSkillAiResourceEntity = ( entity: Entity, -): entity is SkillAIResourceEntityV1alpha1 => - isAIResourceEntity(entity) && entity.spec?.type === 'skill'; +): entity is SkillAiResourceEntityV1alpha1 => + isAiResourceEntity(entity) && entity.spec?.type === 'skill'; const baseRelationFields = [ { selector: { path: 'spec.owner' }, relation: 'ownedBy', defaultKind: 'Group', - defaultNamespace: 'inherit', + defaultNamespace: 'inherit' as const, allowedKinds: ['Group', 'User'], }, { selector: { path: 'spec.system' }, relation: 'partOf', defaultKind: 'System', - defaultNamespace: 'inherit', + defaultNamespace: 'inherit' as const, }, ]; /** - * Extends the catalog model with the AIResource kind. + * Extends the catalog model with the AiResource kind. * * @alpha */ @@ -144,7 +144,7 @@ export const aiResourceEntityModel = createCatalogModelLayer({ model.addKind({ group: 'backstage.io', names: { - kind: 'AIResource', + kind: 'AiResource', singular: 'airesource', plural: 'airesources', }, @@ -166,8 +166,8 @@ export const aiResourceEntityModel = createCatalogModelLayer({ { selector: { path: 'spec.dependsOn' }, relation: 'dependsOn', - defaultKind: 'AIResource', - defaultNamespace: 'inherit', + defaultKind: 'AiResource', + defaultNamespace: 'inherit' as const, }, ], schema: { diff --git a/plugins/catalog-backend-module-ai-resource-entity-model/src/alpha.ts b/plugins/catalog-backend-module-ai-resource-entity-model/src/alpha.ts index 1878461df7..6f9fc0eb7b 100644 --- a/plugins/catalog-backend-module-ai-resource-entity-model/src/alpha.ts +++ b/plugins/catalog-backend-module-ai-resource-entity-model/src/alpha.ts @@ -14,4 +14,4 @@ * limitations under the License. */ -export { aiResourceEntityModel } from './AIResourceEntityV1alpha1'; +export { aiResourceEntityModel } from './AiResourceEntityV1alpha1'; diff --git a/plugins/catalog-backend-module-ai-resource-entity-model/src/index.ts b/plugins/catalog-backend-module-ai-resource-entity-model/src/index.ts index afd2bc82d2..7b6a6cba36 100644 --- a/plugins/catalog-backend-module-ai-resource-entity-model/src/index.ts +++ b/plugins/catalog-backend-module-ai-resource-entity-model/src/index.ts @@ -15,20 +15,20 @@ */ /** - * Adds support for the AIResource entity kind to the catalog backend plugin. + * Adds support for the AiResource entity kind to the catalog backend plugin. * * @packageDocumentation */ export type { - AIResourceEntityV1alpha1, - AIResourceEntityV1alpha1Default, - SkillAIResourceEntityV1alpha1, -} from './AIResourceEntityV1alpha1'; + AiResourceEntityV1alpha1, + AiResourceEntityV1alpha1Default, + SkillAiResourceEntityV1alpha1, +} from './AiResourceEntityV1alpha1'; export { aiResourceEntityV1alpha1Validator, - skillAIResourceEntityV1alpha1Validator, - isAIResourceEntity, - isSkillAIResourceEntity, -} from './AIResourceEntityV1alpha1'; -export { catalogModuleAIResourceEntityModel as default } from './module'; + skillAiResourceEntityV1alpha1Validator, + isAiResourceEntity, + isSkillAiResourceEntity, +} from './AiResourceEntityV1alpha1'; +export { catalogModuleAiResourceEntityModel as default } from './module'; diff --git a/plugins/catalog-backend-module-ai-resource-entity-model/src/module.test.ts b/plugins/catalog-backend-module-ai-resource-entity-model/src/module.test.ts index 030c164d86..89fba1fdc3 100644 --- a/plugins/catalog-backend-module-ai-resource-entity-model/src/module.test.ts +++ b/plugins/catalog-backend-module-ai-resource-entity-model/src/module.test.ts @@ -16,9 +16,9 @@ import { startTestBackend } from '@backstage/backend-test-utils'; import { catalogModelExtensionPoint } from '@backstage/plugin-catalog-node/alpha'; -import { catalogModuleAIResourceEntityModel } from './module'; +import { catalogModuleAiResourceEntityModel } from './module'; -describe('catalogModuleAIResourceEntityModel', () => { +describe('catalogModuleAiResourceEntityModel', () => { it('should register the model source', async () => { const extensionPoint = { setFieldValidators: jest.fn(), @@ -28,7 +28,7 @@ describe('catalogModuleAIResourceEntityModel', () => { await startTestBackend({ extensionPoints: [[catalogModelExtensionPoint, extensionPoint]], - features: [catalogModuleAIResourceEntityModel], + features: [catalogModuleAiResourceEntityModel], }); expect(extensionPoint.addModelSource).toHaveBeenCalledTimes(1); diff --git a/plugins/catalog-backend-module-ai-resource-entity-model/src/module.ts b/plugins/catalog-backend-module-ai-resource-entity-model/src/module.ts index 6ccab581df..2796a84377 100644 --- a/plugins/catalog-backend-module-ai-resource-entity-model/src/module.ts +++ b/plugins/catalog-backend-module-ai-resource-entity-model/src/module.ts @@ -17,14 +17,14 @@ import { createBackendModule } from '@backstage/backend-plugin-api'; import { CatalogModelSources } from '@backstage/catalog-model/alpha'; import { catalogModelExtensionPoint } from '@backstage/plugin-catalog-node/alpha'; -import { aiResourceEntityModel } from './AIResourceEntityV1alpha1'; +import { aiResourceEntityModel } from './AiResourceEntityV1alpha1'; /** - * Registers support for the AIResource entity kind in the catalog. + * Registers support for the AiResource entity kind in the catalog. * * @public */ -export const catalogModuleAIResourceEntityModel = createBackendModule({ +export const catalogModuleAiResourceEntityModel = createBackendModule({ pluginId: 'catalog', moduleId: 'ai-resource-entity-model', register(reg) { diff --git a/plugins/catalog-backend-module-ai-resource-entity-model/src/schema/AIResource.v1alpha1.schema.json b/plugins/catalog-backend-module-ai-resource-entity-model/src/schema/AiResource.v1alpha1.schema.json similarity index 95% rename from plugins/catalog-backend-module-ai-resource-entity-model/src/schema/AIResource.v1alpha1.schema.json rename to plugins/catalog-backend-module-ai-resource-entity-model/src/schema/AiResource.v1alpha1.schema.json index 2b84c00fd1..3077543a64 100644 --- a/plugins/catalog-backend-module-ai-resource-entity-model/src/schema/AIResource.v1alpha1.schema.json +++ b/plugins/catalog-backend-module-ai-resource-entity-model/src/schema/AiResource.v1alpha1.schema.json @@ -1,11 +1,11 @@ { "$schema": "http://json-schema.org/draft-07/schema", - "$id": "AIResourceV1alpha1", + "$id": "AiResourceV1alpha1", "description": "An AI resource represents contextual information consumed by AI coding tools, such as skills and rules.", "examples": [ { "apiVersion": "backstage.io/v1alpha1", - "kind": "AIResource", + "kind": "AiResource", "metadata": { "name": "frontend-design", "description": "Skill for creating production-grade frontend interfaces" @@ -30,7 +30,7 @@ "enum": ["backstage.io/v1alpha1"] }, "kind": { - "enum": ["AIResource"] + "enum": ["AiResource"] }, "spec": { "type": "object", diff --git a/plugins/catalog-backend-module-ai-resource-entity-model/src/schema/AIResource.v1alpha1.skill.schema.json b/plugins/catalog-backend-module-ai-resource-entity-model/src/schema/AiResource.v1alpha1.skill.schema.json similarity index 96% rename from plugins/catalog-backend-module-ai-resource-entity-model/src/schema/AIResource.v1alpha1.skill.schema.json rename to plugins/catalog-backend-module-ai-resource-entity-model/src/schema/AiResource.v1alpha1.skill.schema.json index 32f220582c..374c47f360 100644 --- a/plugins/catalog-backend-module-ai-resource-entity-model/src/schema/AIResource.v1alpha1.skill.schema.json +++ b/plugins/catalog-backend-module-ai-resource-entity-model/src/schema/AiResource.v1alpha1.skill.schema.json @@ -1,11 +1,11 @@ { "$schema": "http://json-schema.org/draft-07/schema", - "$id": "AIResourceV1alpha1Skill", + "$id": "AiResourceV1alpha1Skill", "description": "An AI resource of type 'skill', representing reusable contextual knowledge consumed by AI coding tools.", "examples": [ { "apiVersion": "backstage.io/v1alpha1", - "kind": "AIResource", + "kind": "AiResource", "metadata": { "name": "frontend-design", "description": "Skill for creating production-grade frontend interfaces" @@ -33,7 +33,7 @@ "enum": ["backstage.io/v1alpha1"] }, "kind": { - "enum": ["AIResource"] + "enum": ["AiResource"] }, "spec": { "type": "object", From 46fbd60aefc0140aa226200a79999393e1196f46 Mon Sep 17 00:00:00 2001 From: benjdlambert Date: Mon, 18 May 2026 17:14:43 +0200 Subject: [PATCH 09/15] refactor: rename package to @backstage/plugin-catalog-backend-module-ai-model Signed-off-by: benjdlambert --- .changeset/add-ai-resource-entity-model.md | 2 +- packages/backend/package.json | 2 +- packages/backend/src/index.ts | 4 +--- .../.eslintrc.js | 0 .../README.md | 6 ++---- .../catalog-info.yaml | 4 ++-- .../package.json | 4 ++-- .../report-alpha.api.md | 2 +- .../report.api.md | 2 +- .../src/AiResourceEntityV1alpha1.test.ts | 0 .../src/AiResourceEntityV1alpha1.ts | 0 .../src/alpha.ts | 0 .../src/index.ts | 0 .../src/module.test.ts | 0 .../src/module.ts | 2 +- .../src/schema/AiResource.v1alpha1.schema.json | 0 .../src/schema/AiResource.v1alpha1.skill.schema.json | 0 yarn.lock | 6 +++--- 18 files changed, 15 insertions(+), 19 deletions(-) rename plugins/{catalog-backend-module-ai-resource-entity-model => catalog-backend-module-ai-model}/.eslintrc.js (100%) rename plugins/{catalog-backend-module-ai-resource-entity-model => catalog-backend-module-ai-model}/README.md (86%) rename plugins/{catalog-backend-module-ai-resource-entity-model => catalog-backend-module-ai-model}/catalog-info.yaml (61%) rename plugins/{catalog-backend-module-ai-resource-entity-model => catalog-backend-module-ai-model}/package.json (90%) rename plugins/{catalog-backend-module-ai-resource-entity-model => catalog-backend-module-ai-model}/report-alpha.api.md (93%) rename plugins/{catalog-backend-module-ai-resource-entity-model => catalog-backend-module-ai-model}/report.api.md (98%) rename plugins/{catalog-backend-module-ai-resource-entity-model => catalog-backend-module-ai-model}/src/AiResourceEntityV1alpha1.test.ts (100%) rename plugins/{catalog-backend-module-ai-resource-entity-model => catalog-backend-module-ai-model}/src/AiResourceEntityV1alpha1.ts (100%) rename plugins/{catalog-backend-module-ai-resource-entity-model => catalog-backend-module-ai-model}/src/alpha.ts (100%) rename plugins/{catalog-backend-module-ai-resource-entity-model => catalog-backend-module-ai-model}/src/index.ts (100%) rename plugins/{catalog-backend-module-ai-resource-entity-model => catalog-backend-module-ai-model}/src/module.test.ts (100%) rename plugins/{catalog-backend-module-ai-resource-entity-model => catalog-backend-module-ai-model}/src/module.ts (97%) rename plugins/{catalog-backend-module-ai-resource-entity-model => catalog-backend-module-ai-model}/src/schema/AiResource.v1alpha1.schema.json (100%) rename plugins/{catalog-backend-module-ai-resource-entity-model => catalog-backend-module-ai-model}/src/schema/AiResource.v1alpha1.skill.schema.json (100%) diff --git a/.changeset/add-ai-resource-entity-model.md b/.changeset/add-ai-resource-entity-model.md index 97e6cb9d52..8c3356734b 100644 --- a/.changeset/add-ai-resource-entity-model.md +++ b/.changeset/add-ai-resource-entity-model.md @@ -1,5 +1,5 @@ --- -'@backstage/plugin-catalog-backend-module-ai-resource-entity-model': minor +'@backstage/plugin-catalog-backend-module-ai-model': minor --- Introduced the `AiResource` catalog entity kind as an opt-in backend module. Install this module to register support for `AiResource` entities in your catalog, used to represent contextual information consumed by AI coding tools such as skills and rules. diff --git a/packages/backend/package.json b/packages/backend/package.json index 1ebdb39406..90e4463c37 100644 --- a/packages/backend/package.json +++ b/packages/backend/package.json @@ -40,7 +40,7 @@ "@backstage/plugin-auth-backend-module-openshift-provider": "workspace:^", "@backstage/plugin-auth-node": "workspace:^", "@backstage/plugin-catalog-backend": "workspace:^", - "@backstage/plugin-catalog-backend-module-ai-resource-entity-model": "workspace:^", + "@backstage/plugin-catalog-backend-module-ai-model": "workspace:^", "@backstage/plugin-catalog-backend-module-backstage-openapi": "workspace:^", "@backstage/plugin-catalog-backend-module-logs": "workspace:^", "@backstage/plugin-catalog-backend-module-openapi": "workspace:^", diff --git a/packages/backend/src/index.ts b/packages/backend/src/index.ts index 9a74cf9f51..4152121a45 100644 --- a/packages/backend/src/index.ts +++ b/packages/backend/src/index.ts @@ -46,9 +46,7 @@ backend.add(import('@backstage/plugin-auth-backend-module-guest-provider')); backend.add(import('@backstage/plugin-auth-backend-module-openshift-provider')); backend.add(import('@backstage/plugin-app-backend')); backend.add(import('@backstage/plugin-catalog-backend-module-unprocessed')); -backend.add( - import('@backstage/plugin-catalog-backend-module-ai-resource-entity-model'), -); +backend.add(import('@backstage/plugin-catalog-backend-module-ai-model')); backend.add( import('@backstage/plugin-catalog-backend-module-scaffolder-entity-model'), ); diff --git a/plugins/catalog-backend-module-ai-resource-entity-model/.eslintrc.js b/plugins/catalog-backend-module-ai-model/.eslintrc.js similarity index 100% rename from plugins/catalog-backend-module-ai-resource-entity-model/.eslintrc.js rename to plugins/catalog-backend-module-ai-model/.eslintrc.js diff --git a/plugins/catalog-backend-module-ai-resource-entity-model/README.md b/plugins/catalog-backend-module-ai-model/README.md similarity index 86% rename from plugins/catalog-backend-module-ai-resource-entity-model/README.md rename to plugins/catalog-backend-module-ai-model/README.md index 7d229dd00a..907c074c83 100644 --- a/plugins/catalog-backend-module-ai-resource-entity-model/README.md +++ b/plugins/catalog-backend-module-ai-model/README.md @@ -1,4 +1,4 @@ -# @backstage/plugin-catalog-backend-module-ai-resource-entity-model +# @backstage/plugin-catalog-backend-module-ai-model Adds support for the `AiResource` entity kind to the catalog backend plugin. AI resources represent contextual information consumed by AI coding tools, such as skills and rules. @@ -7,9 +7,7 @@ Adds support for the `AiResource` entity kind to the catalog backend plugin. AI Add the module to your backend: ```ts -backend.add( - import('@backstage/plugin-catalog-backend-module-ai-resource-entity-model'), -); +backend.add(import('@backstage/plugin-catalog-backend-module-ai-model')); ``` ## Entity shape diff --git a/plugins/catalog-backend-module-ai-resource-entity-model/catalog-info.yaml b/plugins/catalog-backend-module-ai-model/catalog-info.yaml similarity index 61% rename from plugins/catalog-backend-module-ai-resource-entity-model/catalog-info.yaml rename to plugins/catalog-backend-module-ai-model/catalog-info.yaml index d6a29b6e18..a99fca43c9 100644 --- a/plugins/catalog-backend-module-ai-resource-entity-model/catalog-info.yaml +++ b/plugins/catalog-backend-module-ai-model/catalog-info.yaml @@ -1,8 +1,8 @@ apiVersion: backstage.io/v1alpha1 kind: Component metadata: - name: backstage-plugin-catalog-backend-module-ai-resource-entity-model - title: '@backstage/plugin-catalog-backend-module-ai-resource-entity-model' + name: backstage-plugin-catalog-backend-module-ai-model + title: "@backstage/plugin-catalog-backend-module-ai-model" description: Adds support for the AIResource entity kind to the catalog backend plugin. spec: lifecycle: experimental diff --git a/plugins/catalog-backend-module-ai-resource-entity-model/package.json b/plugins/catalog-backend-module-ai-model/package.json similarity index 90% rename from plugins/catalog-backend-module-ai-resource-entity-model/package.json rename to plugins/catalog-backend-module-ai-model/package.json index 2ebd5b4bd6..c1cc2dbee5 100644 --- a/plugins/catalog-backend-module-ai-resource-entity-model/package.json +++ b/plugins/catalog-backend-module-ai-model/package.json @@ -1,5 +1,5 @@ { - "name": "@backstage/plugin-catalog-backend-module-ai-resource-entity-model", + "name": "@backstage/plugin-catalog-backend-module-ai-model", "version": "0.1.0", "description": "Adds support for the AIResource entity kind to the catalog backend plugin.", "backstage": { @@ -13,7 +13,7 @@ "repository": { "type": "git", "url": "https://github.com/backstage/backstage", - "directory": "plugins/catalog-backend-module-ai-resource-entity-model" + "directory": "plugins/catalog-backend-module-ai-model" }, "license": "Apache-2.0", "exports": { diff --git a/plugins/catalog-backend-module-ai-resource-entity-model/report-alpha.api.md b/plugins/catalog-backend-module-ai-model/report-alpha.api.md similarity index 93% rename from plugins/catalog-backend-module-ai-resource-entity-model/report-alpha.api.md rename to plugins/catalog-backend-module-ai-model/report-alpha.api.md index 3012dbbfbb..0da8b27915 100644 --- a/plugins/catalog-backend-module-ai-resource-entity-model/report-alpha.api.md +++ b/plugins/catalog-backend-module-ai-model/report-alpha.api.md @@ -1,4 +1,4 @@ -## API Report File for "@backstage/plugin-catalog-backend-module-ai-resource-entity-model" +## API Report File for "@backstage/plugin-catalog-backend-module-ai-model" > Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). diff --git a/plugins/catalog-backend-module-ai-resource-entity-model/report.api.md b/plugins/catalog-backend-module-ai-model/report.api.md similarity index 98% rename from plugins/catalog-backend-module-ai-resource-entity-model/report.api.md rename to plugins/catalog-backend-module-ai-model/report.api.md index ad21f4982f..1e5315d601 100644 --- a/plugins/catalog-backend-module-ai-resource-entity-model/report.api.md +++ b/plugins/catalog-backend-module-ai-model/report.api.md @@ -1,4 +1,4 @@ -## API Report File for "@backstage/plugin-catalog-backend-module-ai-resource-entity-model" +## API Report File for "@backstage/plugin-catalog-backend-module-ai-model" > Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). diff --git a/plugins/catalog-backend-module-ai-resource-entity-model/src/AiResourceEntityV1alpha1.test.ts b/plugins/catalog-backend-module-ai-model/src/AiResourceEntityV1alpha1.test.ts similarity index 100% rename from plugins/catalog-backend-module-ai-resource-entity-model/src/AiResourceEntityV1alpha1.test.ts rename to plugins/catalog-backend-module-ai-model/src/AiResourceEntityV1alpha1.test.ts diff --git a/plugins/catalog-backend-module-ai-resource-entity-model/src/AiResourceEntityV1alpha1.ts b/plugins/catalog-backend-module-ai-model/src/AiResourceEntityV1alpha1.ts similarity index 100% rename from plugins/catalog-backend-module-ai-resource-entity-model/src/AiResourceEntityV1alpha1.ts rename to plugins/catalog-backend-module-ai-model/src/AiResourceEntityV1alpha1.ts diff --git a/plugins/catalog-backend-module-ai-resource-entity-model/src/alpha.ts b/plugins/catalog-backend-module-ai-model/src/alpha.ts similarity index 100% rename from plugins/catalog-backend-module-ai-resource-entity-model/src/alpha.ts rename to plugins/catalog-backend-module-ai-model/src/alpha.ts diff --git a/plugins/catalog-backend-module-ai-resource-entity-model/src/index.ts b/plugins/catalog-backend-module-ai-model/src/index.ts similarity index 100% rename from plugins/catalog-backend-module-ai-resource-entity-model/src/index.ts rename to plugins/catalog-backend-module-ai-model/src/index.ts diff --git a/plugins/catalog-backend-module-ai-resource-entity-model/src/module.test.ts b/plugins/catalog-backend-module-ai-model/src/module.test.ts similarity index 100% rename from plugins/catalog-backend-module-ai-resource-entity-model/src/module.test.ts rename to plugins/catalog-backend-module-ai-model/src/module.test.ts diff --git a/plugins/catalog-backend-module-ai-resource-entity-model/src/module.ts b/plugins/catalog-backend-module-ai-model/src/module.ts similarity index 97% rename from plugins/catalog-backend-module-ai-resource-entity-model/src/module.ts rename to plugins/catalog-backend-module-ai-model/src/module.ts index 2796a84377..fc293889b4 100644 --- a/plugins/catalog-backend-module-ai-resource-entity-model/src/module.ts +++ b/plugins/catalog-backend-module-ai-model/src/module.ts @@ -26,7 +26,7 @@ import { aiResourceEntityModel } from './AiResourceEntityV1alpha1'; */ export const catalogModuleAiResourceEntityModel = createBackendModule({ pluginId: 'catalog', - moduleId: 'ai-resource-entity-model', + moduleId: 'ai-model', register(reg) { reg.registerInit({ deps: { diff --git a/plugins/catalog-backend-module-ai-resource-entity-model/src/schema/AiResource.v1alpha1.schema.json b/plugins/catalog-backend-module-ai-model/src/schema/AiResource.v1alpha1.schema.json similarity index 100% rename from plugins/catalog-backend-module-ai-resource-entity-model/src/schema/AiResource.v1alpha1.schema.json rename to plugins/catalog-backend-module-ai-model/src/schema/AiResource.v1alpha1.schema.json diff --git a/plugins/catalog-backend-module-ai-resource-entity-model/src/schema/AiResource.v1alpha1.skill.schema.json b/plugins/catalog-backend-module-ai-model/src/schema/AiResource.v1alpha1.skill.schema.json similarity index 100% rename from plugins/catalog-backend-module-ai-resource-entity-model/src/schema/AiResource.v1alpha1.skill.schema.json rename to plugins/catalog-backend-module-ai-model/src/schema/AiResource.v1alpha1.skill.schema.json diff --git a/yarn.lock b/yarn.lock index 401c4072d8..3ed08d279b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4759,9 +4759,9 @@ __metadata: languageName: unknown linkType: soft -"@backstage/plugin-catalog-backend-module-ai-resource-entity-model@workspace:^, @backstage/plugin-catalog-backend-module-ai-resource-entity-model@workspace:plugins/catalog-backend-module-ai-resource-entity-model": +"@backstage/plugin-catalog-backend-module-ai-model@workspace:^, @backstage/plugin-catalog-backend-module-ai-model@workspace:plugins/catalog-backend-module-ai-model": version: 0.0.0-use.local - resolution: "@backstage/plugin-catalog-backend-module-ai-resource-entity-model@workspace:plugins/catalog-backend-module-ai-resource-entity-model" + resolution: "@backstage/plugin-catalog-backend-module-ai-model@workspace:plugins/catalog-backend-module-ai-model" dependencies: "@backstage/backend-plugin-api": "workspace:^" "@backstage/backend-test-utils": "workspace:^" @@ -29306,7 +29306,7 @@ __metadata: "@backstage/plugin-auth-backend-module-openshift-provider": "workspace:^" "@backstage/plugin-auth-node": "workspace:^" "@backstage/plugin-catalog-backend": "workspace:^" - "@backstage/plugin-catalog-backend-module-ai-resource-entity-model": "workspace:^" + "@backstage/plugin-catalog-backend-module-ai-model": "workspace:^" "@backstage/plugin-catalog-backend-module-backstage-openapi": "workspace:^" "@backstage/plugin-catalog-backend-module-logs": "workspace:^" "@backstage/plugin-catalog-backend-module-openapi": "workspace:^" From 333b10c2ab7b5b65a3aa3ae35acff008d93e7b2f Mon Sep 17 00:00:00 2001 From: benjdlambert Date: Mon, 18 May 2026 17:38:27 +0200 Subject: [PATCH 10/15] refactor: move AiResource types to @backstage/catalog-model alpha Signed-off-by: benjdlambert --- .changeset/add-ai-resource-entity-model.md | 3 +- packages/catalog-model/report-alpha.api.md | 104 +++++++++++++++++- packages/catalog-model/src/alpha.ts | 19 ++++ .../kinds}/AiResourceEntityV1alpha1.test.ts | 2 +- .../src/kinds}/AiResourceEntityV1alpha1.ts | 28 +++-- .../kinds}/AiResource.v1alpha1.schema.json | 0 .../AiResource.v1alpha1.skill.schema.json | 0 .../catalog-info.yaml | 2 +- .../package.json | 9 +- .../report-alpha.api.md | 12 -- .../report.api.md | 57 ---------- .../src/alpha.ts | 17 --- .../src/index.ts | 11 -- .../src/module.ts | 6 +- yarn.lock | 1 - 15 files changed, 144 insertions(+), 127 deletions(-) rename {plugins/catalog-backend-module-ai-model/src => packages/catalog-model/src/kinds}/AiResourceEntityV1alpha1.test.ts (99%) rename {plugins/catalog-backend-module-ai-model/src => packages/catalog-model/src/kinds}/AiResourceEntityV1alpha1.ts (90%) rename {plugins/catalog-backend-module-ai-model/src/schema => packages/catalog-model/src/schema/kinds}/AiResource.v1alpha1.schema.json (100%) rename {plugins/catalog-backend-module-ai-model/src/schema => packages/catalog-model/src/schema/kinds}/AiResource.v1alpha1.skill.schema.json (100%) delete mode 100644 plugins/catalog-backend-module-ai-model/report-alpha.api.md delete mode 100644 plugins/catalog-backend-module-ai-model/src/alpha.ts diff --git a/.changeset/add-ai-resource-entity-model.md b/.changeset/add-ai-resource-entity-model.md index 8c3356734b..6fcdb79a53 100644 --- a/.changeset/add-ai-resource-entity-model.md +++ b/.changeset/add-ai-resource-entity-model.md @@ -1,5 +1,6 @@ --- '@backstage/plugin-catalog-backend-module-ai-model': minor +'@backstage/catalog-model': minor --- -Introduced the `AiResource` catalog entity kind as an opt-in backend module. Install this module to register support for `AiResource` entities in your catalog, used to represent contextual information consumed by AI coding tools such as skills and rules. +Introduced the `AiResource` catalog entity kind. Entity types, validators, type guards, and the model layer are exported from `@backstage/catalog-model/alpha`. Install `@backstage/plugin-catalog-backend-module-ai-model` in your backend to register the kind with the catalog. diff --git a/packages/catalog-model/report-alpha.api.md b/packages/catalog-model/report-alpha.api.md index 7483d0b6a6..550a79c45c 100644 --- a/packages/catalog-model/report-alpha.api.md +++ b/packages/catalog-model/report-alpha.api.md @@ -3,13 +3,39 @@ > Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). ```ts -import type { Entity } from '@backstage/catalog-model'; +import type { Entity as Entity_2 } from '@backstage/catalog-model'; import { JsonObject } from '@backstage/types'; import { JsonValue } from '@backstage/types'; import { SerializedError } from '@backstage/errors'; // @alpha -export interface AlphaEntity extends Entity { +export const aiResourceEntityModel: CatalogModelLayer; + +// @alpha +export type AiResourceEntityV1alpha1 = + | AiResourceEntityV1alpha1Default + | SkillAiResourceEntityV1alpha1; + +// @alpha +export interface AiResourceEntityV1alpha1Default extends Entity { + // (undocumented) + apiVersion: 'backstage.io/v1alpha1'; + // (undocumented) + kind: 'AiResource'; + // (undocumented) + spec: { + type: string; + lifecycle: string; + owner: string; + system?: string; + }; +} + +// @alpha +export const aiResourceEntityV1alpha1Validator: KindValidator; + +// @alpha +export interface AlphaEntity extends Entity_2 { status?: EntityStatus; } @@ -400,6 +426,43 @@ export function createCatalogModelLayerBuilder(options: { // @alpha export const defaultCatalogEntityModel: CatalogModelLayer; +// @public +export type Entity = { + apiVersion: string; + kind: string; + metadata: EntityMeta; + spec?: JsonObject; + relations?: EntityRelation[]; +}; + +// @public +export type EntityLink = { + url: string; + title?: string; + icon?: string; + type?: string; +}; + +// @public +export type EntityMeta = JsonObject & { + uid?: string; + etag?: string; + name: string; + namespace?: string; + title?: string; + description?: string; + labels?: Record; + annotations?: Record; + tags?: string[]; + links?: EntityLink[]; +}; + +// @public +export type EntityRelation = { + type: string; + targetRef: string; +}; + // @alpha export type EntityStatus = { items?: EntityStatusItem[]; @@ -416,5 +479,42 @@ export type EntityStatusItem = { // @alpha export type EntityStatusLevel = 'info' | 'warning' | 'error'; +// @alpha +export const isAiResourceEntity: ( + entity: Entity, +) => entity is AiResourceEntityV1alpha1; + +// @alpha +export const isSkillAiResourceEntity: ( + entity: Entity, +) => entity is SkillAiResourceEntityV1alpha1; + +// @public +export type KindValidator = { + check(entity: Entity): Promise; +}; + +// @alpha +export interface SkillAiResourceEntityV1alpha1 extends Entity { + // (undocumented) + apiVersion: 'backstage.io/v1alpha1'; + // (undocumented) + kind: 'AiResource'; + // (undocumented) + spec: { + type: 'skill'; + lifecycle: string; + owner: string; + system?: string; + disciplines?: string[]; + categories?: string[]; + agents?: string[]; + dependsOn?: string[]; + }; +} + +// @alpha +export const skillAiResourceEntityV1alpha1Validator: KindValidator; + // (No @packageDocumentation comment for this package) ``` diff --git a/packages/catalog-model/src/alpha.ts b/packages/catalog-model/src/alpha.ts index 9381d92329..d7c7399865 100644 --- a/packages/catalog-model/src/alpha.ts +++ b/packages/catalog-model/src/alpha.ts @@ -14,11 +14,30 @@ * limitations under the License. */ +export type { + Entity, + EntityLink, + EntityMeta, + EntityRelation, +} from './entity/Entity'; +export type { KindValidator } from './kinds/types'; export type { AlphaEntity } from './entity/AlphaEntity'; export type { EntityStatus, EntityStatusItem, EntityStatusLevel, } from './entity/EntityStatus'; +export type { + AiResourceEntityV1alpha1, + AiResourceEntityV1alpha1Default, + SkillAiResourceEntityV1alpha1, +} from './kinds/AiResourceEntityV1alpha1'; +export { + aiResourceEntityV1alpha1Validator, + skillAiResourceEntityV1alpha1Validator, + isAiResourceEntity, + isSkillAiResourceEntity, + aiResourceEntityModel, +} from './kinds/AiResourceEntityV1alpha1'; export * from './model'; export { defaultCatalogEntityModel } from './model/defaultCatalogEntityModel'; diff --git a/plugins/catalog-backend-module-ai-model/src/AiResourceEntityV1alpha1.test.ts b/packages/catalog-model/src/kinds/AiResourceEntityV1alpha1.test.ts similarity index 99% rename from plugins/catalog-backend-module-ai-model/src/AiResourceEntityV1alpha1.test.ts rename to packages/catalog-model/src/kinds/AiResourceEntityV1alpha1.test.ts index 308dc1517a..aa75b414c5 100644 --- a/plugins/catalog-backend-module-ai-model/src/AiResourceEntityV1alpha1.test.ts +++ b/packages/catalog-model/src/kinds/AiResourceEntityV1alpha1.test.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import type { Entity } from '@backstage/catalog-model'; +import type { Entity } from '../entity/Entity'; import { type AiResourceEntityV1alpha1Default, type SkillAiResourceEntityV1alpha1, diff --git a/plugins/catalog-backend-module-ai-model/src/AiResourceEntityV1alpha1.ts b/packages/catalog-model/src/kinds/AiResourceEntityV1alpha1.ts similarity index 90% rename from plugins/catalog-backend-module-ai-model/src/AiResourceEntityV1alpha1.ts rename to packages/catalog-model/src/kinds/AiResourceEntityV1alpha1.ts index dac2c3c421..53fa20d17a 100644 --- a/plugins/catalog-backend-module-ai-model/src/AiResourceEntityV1alpha1.ts +++ b/packages/catalog-model/src/kinds/AiResourceEntityV1alpha1.ts @@ -14,20 +14,18 @@ * limitations under the License. */ -import { - type Entity, - entityKindSchemaValidator, - type KindValidator, -} from '@backstage/catalog-model'; -import { createCatalogModelLayer } from '@backstage/catalog-model/alpha'; +import { createCatalogModelLayer } from '../model/createCatalogModelLayer'; +import type { Entity } from '../entity/Entity'; +import { entityKindSchemaValidator } from '../validation'; +import type { KindValidator } from './types'; import type { JsonObject } from '@backstage/types'; -import defaultJsonSchema from './schema/AiResource.v1alpha1.schema.json'; -import skillJsonSchema from './schema/AiResource.v1alpha1.skill.schema.json'; +import defaultJsonSchema from '../schema/kinds/AiResource.v1alpha1.schema.json'; +import skillJsonSchema from '../schema/kinds/AiResource.v1alpha1.skill.schema.json'; /** * Default AiResource entity for types that don't have a structured spec. * - * @public + * @alpha */ export interface AiResourceEntityV1alpha1Default extends Entity { apiVersion: 'backstage.io/v1alpha1'; @@ -44,7 +42,7 @@ export interface AiResourceEntityV1alpha1Default extends Entity { * AiResource entity with spec.type 'skill'. Represents reusable contextual * knowledge consumed by AI coding tools. * - * @public + * @alpha */ export interface SkillAiResourceEntityV1alpha1 extends Entity { apiVersion: 'backstage.io/v1alpha1'; @@ -65,7 +63,7 @@ export interface SkillAiResourceEntityV1alpha1 extends Entity { * Backstage catalog AiResource kind Entity. Represents contextual information * consumed by AI coding tools, such as skills and rules. * - * @public + * @alpha */ export type AiResourceEntityV1alpha1 = | AiResourceEntityV1alpha1Default @@ -76,7 +74,7 @@ const defaultValidator = entityKindSchemaValidator(defaultJsonSchema); /** * Entity data validator for the default {@link AiResourceEntityV1alpha1}. * - * @public + * @alpha */ export const aiResourceEntityV1alpha1Validator: KindValidator = { async check(data: Entity) { @@ -89,7 +87,7 @@ const skillValidator = entityKindSchemaValidator(skillJsonSchema); /** * Entity data validator for {@link SkillAiResourceEntityV1alpha1}. * - * @public + * @alpha */ export const skillAiResourceEntityV1alpha1Validator: KindValidator = { async check(data: Entity) { @@ -100,7 +98,7 @@ export const skillAiResourceEntityV1alpha1Validator: KindValidator = { /** * Type guard for {@link AiResourceEntityV1alpha1}. * - * @public + * @alpha */ export const isAiResourceEntity = ( entity: Entity, @@ -110,7 +108,7 @@ export const isAiResourceEntity = ( /** * Type guard for {@link SkillAiResourceEntityV1alpha1}. * - * @public + * @alpha */ export const isSkillAiResourceEntity = ( entity: Entity, diff --git a/plugins/catalog-backend-module-ai-model/src/schema/AiResource.v1alpha1.schema.json b/packages/catalog-model/src/schema/kinds/AiResource.v1alpha1.schema.json similarity index 100% rename from plugins/catalog-backend-module-ai-model/src/schema/AiResource.v1alpha1.schema.json rename to packages/catalog-model/src/schema/kinds/AiResource.v1alpha1.schema.json diff --git a/plugins/catalog-backend-module-ai-model/src/schema/AiResource.v1alpha1.skill.schema.json b/packages/catalog-model/src/schema/kinds/AiResource.v1alpha1.skill.schema.json similarity index 100% rename from plugins/catalog-backend-module-ai-model/src/schema/AiResource.v1alpha1.skill.schema.json rename to packages/catalog-model/src/schema/kinds/AiResource.v1alpha1.skill.schema.json diff --git a/plugins/catalog-backend-module-ai-model/catalog-info.yaml b/plugins/catalog-backend-module-ai-model/catalog-info.yaml index a99fca43c9..a9b022ecb5 100644 --- a/plugins/catalog-backend-module-ai-model/catalog-info.yaml +++ b/plugins/catalog-backend-module-ai-model/catalog-info.yaml @@ -2,7 +2,7 @@ apiVersion: backstage.io/v1alpha1 kind: Component metadata: name: backstage-plugin-catalog-backend-module-ai-model - title: "@backstage/plugin-catalog-backend-module-ai-model" + title: '@backstage/plugin-catalog-backend-module-ai-model' description: Adds support for the AIResource entity kind to the catalog backend plugin. spec: lifecycle: experimental diff --git a/plugins/catalog-backend-module-ai-model/package.json b/plugins/catalog-backend-module-ai-model/package.json index c1cc2dbee5..ca6159fba6 100644 --- a/plugins/catalog-backend-module-ai-model/package.json +++ b/plugins/catalog-backend-module-ai-model/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-catalog-backend-module-ai-model", "version": "0.1.0", - "description": "Adds support for the AIResource entity kind to the catalog backend plugin.", + "description": "Adds support for the AiResource entity kind to the catalog backend plugin.", "backstage": { "role": "backend-plugin-module", "pluginId": "catalog", @@ -18,16 +18,12 @@ "license": "Apache-2.0", "exports": { ".": "./src/index.ts", - "./alpha": "./src/alpha.ts", "./package.json": "./package.json" }, "main": "src/index.ts", "types": "src/index.ts", "typesVersions": { "*": { - "alpha": [ - "src/alpha.ts" - ], "package.json": [ "package.json" ] @@ -48,8 +44,7 @@ "dependencies": { "@backstage/backend-plugin-api": "workspace:^", "@backstage/catalog-model": "workspace:^", - "@backstage/plugin-catalog-node": "workspace:^", - "@backstage/types": "workspace:^" + "@backstage/plugin-catalog-node": "workspace:^" }, "devDependencies": { "@backstage/backend-test-utils": "workspace:^", diff --git a/plugins/catalog-backend-module-ai-model/report-alpha.api.md b/plugins/catalog-backend-module-ai-model/report-alpha.api.md deleted file mode 100644 index 0da8b27915..0000000000 --- a/plugins/catalog-backend-module-ai-model/report-alpha.api.md +++ /dev/null @@ -1,12 +0,0 @@ -## API Report File for "@backstage/plugin-catalog-backend-module-ai-model" - -> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). - -```ts -import { CatalogModelLayer } from '@backstage/catalog-model/alpha'; - -// @alpha -export const aiResourceEntityModel: CatalogModelLayer; - -// (No @packageDocumentation comment for this package) -``` diff --git a/plugins/catalog-backend-module-ai-model/report.api.md b/plugins/catalog-backend-module-ai-model/report.api.md index 1e5315d601..b1249ee146 100644 --- a/plugins/catalog-backend-module-ai-model/report.api.md +++ b/plugins/catalog-backend-module-ai-model/report.api.md @@ -4,65 +4,8 @@ ```ts import { BackendFeature } from '@backstage/backend-plugin-api'; -import { Entity } from '@backstage/catalog-model'; -import { KindValidator } from '@backstage/catalog-model'; - -// @public -export type AiResourceEntityV1alpha1 = - | AiResourceEntityV1alpha1Default - | SkillAiResourceEntityV1alpha1; - -// @public -export interface AiResourceEntityV1alpha1Default extends Entity { - // (undocumented) - apiVersion: 'backstage.io/v1alpha1'; - // (undocumented) - kind: 'AiResource'; - // (undocumented) - spec: { - type: string; - lifecycle: string; - owner: string; - system?: string; - }; -} - -// @public -export const aiResourceEntityV1alpha1Validator: KindValidator; // @public const catalogModuleAiResourceEntityModel: BackendFeature; export default catalogModuleAiResourceEntityModel; - -// @public -export const isAiResourceEntity: ( - entity: Entity, -) => entity is AiResourceEntityV1alpha1; - -// @public -export const isSkillAiResourceEntity: ( - entity: Entity, -) => entity is SkillAiResourceEntityV1alpha1; - -// @public -export interface SkillAiResourceEntityV1alpha1 extends Entity { - // (undocumented) - apiVersion: 'backstage.io/v1alpha1'; - // (undocumented) - kind: 'AiResource'; - // (undocumented) - spec: { - type: 'skill'; - lifecycle: string; - owner: string; - system?: string; - disciplines?: string[]; - categories?: string[]; - agents?: string[]; - dependsOn?: string[]; - }; -} - -// @public -export const skillAiResourceEntityV1alpha1Validator: KindValidator; ``` diff --git a/plugins/catalog-backend-module-ai-model/src/alpha.ts b/plugins/catalog-backend-module-ai-model/src/alpha.ts deleted file mode 100644 index 6f9fc0eb7b..0000000000 --- a/plugins/catalog-backend-module-ai-model/src/alpha.ts +++ /dev/null @@ -1,17 +0,0 @@ -/* - * Copyright 2026 The Backstage Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -export { aiResourceEntityModel } from './AiResourceEntityV1alpha1'; diff --git a/plugins/catalog-backend-module-ai-model/src/index.ts b/plugins/catalog-backend-module-ai-model/src/index.ts index 7b6a6cba36..fedbd274eb 100644 --- a/plugins/catalog-backend-module-ai-model/src/index.ts +++ b/plugins/catalog-backend-module-ai-model/src/index.ts @@ -20,15 +20,4 @@ * @packageDocumentation */ -export type { - AiResourceEntityV1alpha1, - AiResourceEntityV1alpha1Default, - SkillAiResourceEntityV1alpha1, -} from './AiResourceEntityV1alpha1'; -export { - aiResourceEntityV1alpha1Validator, - skillAiResourceEntityV1alpha1Validator, - isAiResourceEntity, - isSkillAiResourceEntity, -} from './AiResourceEntityV1alpha1'; export { catalogModuleAiResourceEntityModel as default } from './module'; diff --git a/plugins/catalog-backend-module-ai-model/src/module.ts b/plugins/catalog-backend-module-ai-model/src/module.ts index fc293889b4..f2807a1cab 100644 --- a/plugins/catalog-backend-module-ai-model/src/module.ts +++ b/plugins/catalog-backend-module-ai-model/src/module.ts @@ -15,9 +15,11 @@ */ import { createBackendModule } from '@backstage/backend-plugin-api'; -import { CatalogModelSources } from '@backstage/catalog-model/alpha'; +import { + CatalogModelSources, + aiResourceEntityModel, +} from '@backstage/catalog-model/alpha'; import { catalogModelExtensionPoint } from '@backstage/plugin-catalog-node/alpha'; -import { aiResourceEntityModel } from './AiResourceEntityV1alpha1'; /** * Registers support for the AiResource entity kind in the catalog. diff --git a/yarn.lock b/yarn.lock index 3ed08d279b..6655516238 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4768,7 +4768,6 @@ __metadata: "@backstage/catalog-model": "workspace:^" "@backstage/cli": "workspace:^" "@backstage/plugin-catalog-node": "workspace:^" - "@backstage/types": "workspace:^" languageName: unknown linkType: soft From 21835f6f52f8beb8aa2d8d31d57240ab6b33348c Mon Sep 17 00:00:00 2001 From: benjdlambert Date: Tue, 19 May 2026 10:48:59 +0200 Subject: [PATCH 11/15] feat: add rule specType with disciplines, category, and rationale Signed-off-by: benjdlambert --- .../ai-resources/use-internal-apis-rule.yaml | 5 + packages/catalog-model/report-alpha.api.md | 29 +++++- packages/catalog-model/src/alpha.ts | 3 + .../kinds/AiResourceEntityV1alpha1.test.ts | 98 +++++++++++++++++++ .../src/kinds/AiResourceEntityV1alpha1.ts | 55 ++++++++++- .../AiResource.v1alpha1.rule.schema.json | 87 ++++++++++++++++ 6 files changed, 275 insertions(+), 2 deletions(-) create mode 100644 packages/catalog-model/src/schema/kinds/AiResource.v1alpha1.rule.schema.json diff --git a/packages/catalog-model/examples/ai-resources/use-internal-apis-rule.yaml b/packages/catalog-model/examples/ai-resources/use-internal-apis-rule.yaml index bd06d5bb22..06e08bc959 100644 --- a/packages/catalog-model/examples/ai-resources/use-internal-apis-rule.yaml +++ b/packages/catalog-model/examples/ai-resources/use-internal-apis-rule.yaml @@ -8,3 +8,8 @@ spec: lifecycle: production owner: team-a system: artist-engagement-portal + disciplines: + - web + - backend + category: architecture + rationale: Ensures consistent error handling, authentication, and observability across all service calls diff --git a/packages/catalog-model/report-alpha.api.md b/packages/catalog-model/report-alpha.api.md index 550a79c45c..dfd6882004 100644 --- a/packages/catalog-model/report-alpha.api.md +++ b/packages/catalog-model/report-alpha.api.md @@ -14,7 +14,8 @@ export const aiResourceEntityModel: CatalogModelLayer; // @alpha export type AiResourceEntityV1alpha1 = | AiResourceEntityV1alpha1Default - | SkillAiResourceEntityV1alpha1; + | SkillAiResourceEntityV1alpha1 + | RuleAiResourceEntityV1alpha1; // @alpha export interface AiResourceEntityV1alpha1Default extends Entity { @@ -484,6 +485,11 @@ export const isAiResourceEntity: ( entity: Entity, ) => entity is AiResourceEntityV1alpha1; +// @alpha +export const isRuleAiResourceEntity: ( + entity: Entity, +) => entity is RuleAiResourceEntityV1alpha1; + // @alpha export const isSkillAiResourceEntity: ( entity: Entity, @@ -494,6 +500,27 @@ export type KindValidator = { check(entity: Entity): Promise; }; +// @alpha +export interface RuleAiResourceEntityV1alpha1 extends Entity { + // (undocumented) + apiVersion: 'backstage.io/v1alpha1'; + // (undocumented) + kind: 'AiResource'; + // (undocumented) + spec: { + type: 'rule'; + lifecycle: string; + owner: string; + system?: string; + disciplines?: string[]; + category: string; + rationale: string; + }; +} + +// @alpha +export const ruleAiResourceEntityV1alpha1Validator: KindValidator; + // @alpha export interface SkillAiResourceEntityV1alpha1 extends Entity { // (undocumented) diff --git a/packages/catalog-model/src/alpha.ts b/packages/catalog-model/src/alpha.ts index d7c7399865..219513f167 100644 --- a/packages/catalog-model/src/alpha.ts +++ b/packages/catalog-model/src/alpha.ts @@ -31,12 +31,15 @@ export type { AiResourceEntityV1alpha1, AiResourceEntityV1alpha1Default, SkillAiResourceEntityV1alpha1, + RuleAiResourceEntityV1alpha1, } from './kinds/AiResourceEntityV1alpha1'; export { aiResourceEntityV1alpha1Validator, skillAiResourceEntityV1alpha1Validator, + ruleAiResourceEntityV1alpha1Validator, isAiResourceEntity, isSkillAiResourceEntity, + isRuleAiResourceEntity, aiResourceEntityModel, } from './kinds/AiResourceEntityV1alpha1'; export * from './model'; diff --git a/packages/catalog-model/src/kinds/AiResourceEntityV1alpha1.test.ts b/packages/catalog-model/src/kinds/AiResourceEntityV1alpha1.test.ts index aa75b414c5..e3fa82754a 100644 --- a/packages/catalog-model/src/kinds/AiResourceEntityV1alpha1.test.ts +++ b/packages/catalog-model/src/kinds/AiResourceEntityV1alpha1.test.ts @@ -18,10 +18,13 @@ import type { Entity } from '../entity/Entity'; import { type AiResourceEntityV1alpha1Default, type SkillAiResourceEntityV1alpha1, + type RuleAiResourceEntityV1alpha1, aiResourceEntityV1alpha1Validator as defaultValidator, skillAiResourceEntityV1alpha1Validator as skillValidator, + ruleAiResourceEntityV1alpha1Validator as ruleValidator, isAiResourceEntity, isSkillAiResourceEntity, + isRuleAiResourceEntity, } from './AiResourceEntityV1alpha1'; describe('AiResourceV1alpha1 default validator', () => { @@ -199,6 +202,101 @@ describe('AiResourceV1alpha1 skill validator', () => { }); }); +describe('AiResourceV1alpha1 rule validator', () => { + let entity: RuleAiResourceEntityV1alpha1; + + beforeEach(() => { + entity = { + apiVersion: 'backstage.io/v1alpha1', + kind: 'AiResource', + metadata: { + name: 'use-internal-apis', + }, + spec: { + type: 'rule', + lifecycle: 'production', + owner: 'frontend-platform', + disciplines: ['web', 'backend'], + category: 'architecture', + rationale: 'Ensures consistent error handling across all service calls', + }, + }; + }); + + it('accepts valid rule data with all fields', async () => { + await expect(ruleValidator.check(entity)).resolves.toBe(true); + }); + + it('accepts rule with only required fields', async () => { + entity.spec = { + type: 'rule', + lifecycle: 'production', + owner: 'team-a', + category: 'security', + rationale: 'Prevents credential leaks', + }; + await expect(ruleValidator.check(entity)).resolves.toBe(true); + }); + + it('rejects non-rule type', async () => { + (entity as any).spec.type = 'skill'; + await expect(ruleValidator.check(entity)).rejects.toThrow(/type/); + }); + + it('rejects missing category', async () => { + delete (entity as any).spec.category; + await expect(ruleValidator.check(entity)).rejects.toThrow(/category/); + }); + + it('rejects empty category', async () => { + (entity as any).spec.category = ''; + await expect(ruleValidator.check(entity)).rejects.toThrow(/category/); + }); + + it('rejects missing rationale', async () => { + delete (entity as any).spec.rationale; + await expect(ruleValidator.check(entity)).rejects.toThrow(/rationale/); + }); + + it('rejects empty rationale', async () => { + (entity as any).spec.rationale = ''; + await expect(ruleValidator.check(entity)).rejects.toThrow(/rationale/); + }); + + it('accepts missing optional fields', async () => { + delete (entity as any).spec.system; + delete (entity as any).spec.disciplines; + await expect(ruleValidator.check(entity)).resolves.toBe(true); + }); + + it('rejects disciplines with empty strings', async () => { + (entity as any).spec.disciplines = ['']; + await expect(ruleValidator.check(entity)).rejects.toThrow(/disciplines/); + }); +}); + +describe('isRuleAiResourceEntity', () => { + it('returns true for a rule AiResource', () => { + const entity: Entity = { + apiVersion: 'backstage.io/v1alpha1', + kind: 'AiResource', + metadata: { name: 'test' }, + spec: { type: 'rule' }, + }; + expect(isRuleAiResourceEntity(entity)).toBe(true); + }); + + it('returns false for a non-rule AiResource', () => { + const entity: Entity = { + apiVersion: 'backstage.io/v1alpha1', + kind: 'AiResource', + metadata: { name: 'test' }, + spec: { type: 'skill' }, + }; + expect(isRuleAiResourceEntity(entity)).toBe(false); + }); +}); + describe('isAiResourceEntity', () => { it('returns true when apiVersion and kind match', () => { const entity: Entity = { diff --git a/packages/catalog-model/src/kinds/AiResourceEntityV1alpha1.ts b/packages/catalog-model/src/kinds/AiResourceEntityV1alpha1.ts index 53fa20d17a..1655e0fdbc 100644 --- a/packages/catalog-model/src/kinds/AiResourceEntityV1alpha1.ts +++ b/packages/catalog-model/src/kinds/AiResourceEntityV1alpha1.ts @@ -21,6 +21,7 @@ import type { KindValidator } from './types'; import type { JsonObject } from '@backstage/types'; import defaultJsonSchema from '../schema/kinds/AiResource.v1alpha1.schema.json'; import skillJsonSchema from '../schema/kinds/AiResource.v1alpha1.skill.schema.json'; +import ruleJsonSchema from '../schema/kinds/AiResource.v1alpha1.rule.schema.json'; /** * Default AiResource entity for types that don't have a structured spec. @@ -59,6 +60,26 @@ export interface SkillAiResourceEntityV1alpha1 extends Entity { }; } +/** + * AiResource entity with spec.type 'rule'. Represents a governance rule + * or constraint for AI coding tools. + * + * @alpha + */ +export interface RuleAiResourceEntityV1alpha1 extends Entity { + apiVersion: 'backstage.io/v1alpha1'; + kind: 'AiResource'; + spec: { + type: 'rule'; + lifecycle: string; + owner: string; + system?: string; + disciplines?: string[]; + category: string; + rationale: string; + }; +} + /** * Backstage catalog AiResource kind Entity. Represents contextual information * consumed by AI coding tools, such as skills and rules. @@ -67,7 +88,8 @@ export interface SkillAiResourceEntityV1alpha1 extends Entity { */ export type AiResourceEntityV1alpha1 = | AiResourceEntityV1alpha1Default - | SkillAiResourceEntityV1alpha1; + | SkillAiResourceEntityV1alpha1 + | RuleAiResourceEntityV1alpha1; const defaultValidator = entityKindSchemaValidator(defaultJsonSchema); @@ -115,6 +137,29 @@ export const isSkillAiResourceEntity = ( ): entity is SkillAiResourceEntityV1alpha1 => isAiResourceEntity(entity) && entity.spec?.type === 'skill'; +const ruleValidator = entityKindSchemaValidator(ruleJsonSchema); + +/** + * Entity data validator for {@link RuleAiResourceEntityV1alpha1}. + * + * @alpha + */ +export const ruleAiResourceEntityV1alpha1Validator: KindValidator = { + async check(data: Entity) { + return ruleValidator(data) === data; + }, +}; + +/** + * Type guard for {@link RuleAiResourceEntityV1alpha1}. + * + * @alpha + */ +export const isRuleAiResourceEntity = ( + entity: Entity, +): entity is RuleAiResourceEntityV1alpha1 => + isAiResourceEntity(entity) && entity.spec?.type === 'rule'; + const baseRelationFields = [ { selector: { path: 'spec.owner' }, @@ -172,6 +217,14 @@ export const aiResourceEntityModel = createCatalogModelLayer({ jsonSchema: skillJsonSchema as JsonObject, }, }, + { + name: 'v1alpha1', + specType: 'rule', + relationFields: baseRelationFields, + schema: { + jsonSchema: ruleJsonSchema as JsonObject, + }, + }, ], }); }, diff --git a/packages/catalog-model/src/schema/kinds/AiResource.v1alpha1.rule.schema.json b/packages/catalog-model/src/schema/kinds/AiResource.v1alpha1.rule.schema.json new file mode 100644 index 0000000000..48c0f3ee1e --- /dev/null +++ b/packages/catalog-model/src/schema/kinds/AiResource.v1alpha1.rule.schema.json @@ -0,0 +1,87 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema", + "$id": "AiResourceV1alpha1Rule", + "description": "An AI resource of type 'rule', representing a governance rule or constraint for AI coding tools.", + "examples": [ + { + "apiVersion": "backstage.io/v1alpha1", + "kind": "AiResource", + "metadata": { + "name": "use-internal-apis", + "description": "Agents must use internal API clients instead of raw HTTP calls" + }, + "spec": { + "type": "rule", + "lifecycle": "production", + "owner": "frontend-platform", + "disciplines": ["web", "backend"], + "category": "architecture", + "rationale": "Ensures consistent error handling, authentication, and observability across all service calls" + } + } + ], + "allOf": [ + { + "$ref": "Entity" + }, + { + "type": "object", + "required": ["spec"], + "properties": { + "apiVersion": { + "enum": ["backstage.io/v1alpha1"] + }, + "kind": { + "enum": ["AiResource"] + }, + "spec": { + "type": "object", + "required": ["type", "lifecycle", "owner", "category", "rationale"], + "properties": { + "type": { + "type": "string", + "enum": ["rule"], + "description": "The type of AI resource." + }, + "lifecycle": { + "type": "string", + "description": "The lifecycle state of the AI resource.", + "examples": ["experimental", "production", "deprecated"], + "minLength": 1 + }, + "owner": { + "type": "string", + "description": "An entity reference to the owner of the AI resource.", + "examples": ["frontend-platform", "user:john.johnson"], + "minLength": 1 + }, + "system": { + "type": "string", + "description": "An entity reference to the system that the AI resource belongs to.", + "minLength": 1 + }, + "disciplines": { + "type": "array", + "description": "Engineering disciplines this rule applies to.", + "items": { + "type": "string", + "minLength": 1 + } + }, + "category": { + "type": "string", + "description": "The category of this rule.", + "examples": ["security", "architecture", "style"], + "minLength": 1 + }, + "rationale": { + "type": "string", + "description": "Explanation of why this rule exists.", + "minLength": 1 + } + } + } + } + } + ] +} From 9c7cb8f23297feb683badd6ef6f5a002e824dd2b Mon Sep 17 00:00:00 2001 From: benjdlambert Date: Tue, 19 May 2026 11:05:59 +0200 Subject: [PATCH 12/15] fix: set initial package version to 0.0.0 Signed-off-by: benjdlambert --- plugins/catalog-backend-module-ai-model/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/catalog-backend-module-ai-model/package.json b/plugins/catalog-backend-module-ai-model/package.json index ca6159fba6..540afa2798 100644 --- a/plugins/catalog-backend-module-ai-model/package.json +++ b/plugins/catalog-backend-module-ai-model/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-catalog-backend-module-ai-model", - "version": "0.1.0", + "version": "0.0.0", "description": "Adds support for the AiResource entity kind to the catalog backend plugin.", "backstage": { "role": "backend-plugin-module", From e5671e44c095b61ffe633808e708ec7a7e29d6cb Mon Sep 17 00:00:00 2001 From: benjdlambert Date: Tue, 19 May 2026 12:09:21 +0200 Subject: [PATCH 13/15] refactor: extend default type and remove per-type guards Signed-off-by: benjdlambert --- packages/catalog-model/report-alpha.api.md | 24 ++------ packages/catalog-model/src/alpha.ts | 2 - .../kinds/AiResourceEntityV1alpha1.test.ts | 56 ------------------- .../src/kinds/AiResourceEntityV1alpha1.ts | 30 ++-------- 4 files changed, 8 insertions(+), 104 deletions(-) diff --git a/packages/catalog-model/report-alpha.api.md b/packages/catalog-model/report-alpha.api.md index dfd6882004..4d264e6793 100644 --- a/packages/catalog-model/report-alpha.api.md +++ b/packages/catalog-model/report-alpha.api.md @@ -485,27 +485,14 @@ export const isAiResourceEntity: ( entity: Entity, ) => entity is AiResourceEntityV1alpha1; -// @alpha -export const isRuleAiResourceEntity: ( - entity: Entity, -) => entity is RuleAiResourceEntityV1alpha1; - -// @alpha -export const isSkillAiResourceEntity: ( - entity: Entity, -) => entity is SkillAiResourceEntityV1alpha1; - // @public export type KindValidator = { check(entity: Entity): Promise; }; // @alpha -export interface RuleAiResourceEntityV1alpha1 extends Entity { - // (undocumented) - apiVersion: 'backstage.io/v1alpha1'; - // (undocumented) - kind: 'AiResource'; +export interface RuleAiResourceEntityV1alpha1 + extends AiResourceEntityV1alpha1Default { // (undocumented) spec: { type: 'rule'; @@ -522,11 +509,8 @@ export interface RuleAiResourceEntityV1alpha1 extends Entity { export const ruleAiResourceEntityV1alpha1Validator: KindValidator; // @alpha -export interface SkillAiResourceEntityV1alpha1 extends Entity { - // (undocumented) - apiVersion: 'backstage.io/v1alpha1'; - // (undocumented) - kind: 'AiResource'; +export interface SkillAiResourceEntityV1alpha1 + extends AiResourceEntityV1alpha1Default { // (undocumented) spec: { type: 'skill'; diff --git a/packages/catalog-model/src/alpha.ts b/packages/catalog-model/src/alpha.ts index 219513f167..1d8f1c01af 100644 --- a/packages/catalog-model/src/alpha.ts +++ b/packages/catalog-model/src/alpha.ts @@ -38,8 +38,6 @@ export { skillAiResourceEntityV1alpha1Validator, ruleAiResourceEntityV1alpha1Validator, isAiResourceEntity, - isSkillAiResourceEntity, - isRuleAiResourceEntity, aiResourceEntityModel, } from './kinds/AiResourceEntityV1alpha1'; export * from './model'; diff --git a/packages/catalog-model/src/kinds/AiResourceEntityV1alpha1.test.ts b/packages/catalog-model/src/kinds/AiResourceEntityV1alpha1.test.ts index e3fa82754a..688f25f5c6 100644 --- a/packages/catalog-model/src/kinds/AiResourceEntityV1alpha1.test.ts +++ b/packages/catalog-model/src/kinds/AiResourceEntityV1alpha1.test.ts @@ -23,8 +23,6 @@ import { skillAiResourceEntityV1alpha1Validator as skillValidator, ruleAiResourceEntityV1alpha1Validator as ruleValidator, isAiResourceEntity, - isSkillAiResourceEntity, - isRuleAiResourceEntity, } from './AiResourceEntityV1alpha1'; describe('AiResourceV1alpha1 default validator', () => { @@ -275,28 +273,6 @@ describe('AiResourceV1alpha1 rule validator', () => { }); }); -describe('isRuleAiResourceEntity', () => { - it('returns true for a rule AiResource', () => { - const entity: Entity = { - apiVersion: 'backstage.io/v1alpha1', - kind: 'AiResource', - metadata: { name: 'test' }, - spec: { type: 'rule' }, - }; - expect(isRuleAiResourceEntity(entity)).toBe(true); - }); - - it('returns false for a non-rule AiResource', () => { - const entity: Entity = { - apiVersion: 'backstage.io/v1alpha1', - kind: 'AiResource', - metadata: { name: 'test' }, - spec: { type: 'skill' }, - }; - expect(isRuleAiResourceEntity(entity)).toBe(false); - }); -}); - describe('isAiResourceEntity', () => { it('returns true when apiVersion and kind match', () => { const entity: Entity = { @@ -325,35 +301,3 @@ describe('isAiResourceEntity', () => { expect(isAiResourceEntity(entity)).toBe(false); }); }); - -describe('isSkillAiResourceEntity', () => { - it('returns true for a skill AiResource', () => { - const entity: Entity = { - apiVersion: 'backstage.io/v1alpha1', - kind: 'AiResource', - metadata: { name: 'test' }, - spec: { type: 'skill' }, - }; - expect(isSkillAiResourceEntity(entity)).toBe(true); - }); - - it('returns false for a non-skill AiResource', () => { - const entity: Entity = { - apiVersion: 'backstage.io/v1alpha1', - kind: 'AiResource', - metadata: { name: 'test' }, - spec: { type: 'rule' }, - }; - expect(isSkillAiResourceEntity(entity)).toBe(false); - }); - - it('returns false for a different kind', () => { - const entity: Entity = { - apiVersion: 'backstage.io/v1alpha1', - kind: 'Component', - metadata: { name: 'test' }, - spec: { type: 'skill' }, - }; - expect(isSkillAiResourceEntity(entity)).toBe(false); - }); -}); diff --git a/packages/catalog-model/src/kinds/AiResourceEntityV1alpha1.ts b/packages/catalog-model/src/kinds/AiResourceEntityV1alpha1.ts index 1655e0fdbc..109712c5ec 100644 --- a/packages/catalog-model/src/kinds/AiResourceEntityV1alpha1.ts +++ b/packages/catalog-model/src/kinds/AiResourceEntityV1alpha1.ts @@ -45,9 +45,8 @@ export interface AiResourceEntityV1alpha1Default extends Entity { * * @alpha */ -export interface SkillAiResourceEntityV1alpha1 extends Entity { - apiVersion: 'backstage.io/v1alpha1'; - kind: 'AiResource'; +export interface SkillAiResourceEntityV1alpha1 + extends AiResourceEntityV1alpha1Default { spec: { type: 'skill'; lifecycle: string; @@ -66,9 +65,8 @@ export interface SkillAiResourceEntityV1alpha1 extends Entity { * * @alpha */ -export interface RuleAiResourceEntityV1alpha1 extends Entity { - apiVersion: 'backstage.io/v1alpha1'; - kind: 'AiResource'; +export interface RuleAiResourceEntityV1alpha1 + extends AiResourceEntityV1alpha1Default { spec: { type: 'rule'; lifecycle: string; @@ -127,16 +125,6 @@ export const isAiResourceEntity = ( ): entity is AiResourceEntityV1alpha1 => entity.apiVersion === 'backstage.io/v1alpha1' && entity.kind === 'AiResource'; -/** - * Type guard for {@link SkillAiResourceEntityV1alpha1}. - * - * @alpha - */ -export const isSkillAiResourceEntity = ( - entity: Entity, -): entity is SkillAiResourceEntityV1alpha1 => - isAiResourceEntity(entity) && entity.spec?.type === 'skill'; - const ruleValidator = entityKindSchemaValidator(ruleJsonSchema); /** @@ -150,16 +138,6 @@ export const ruleAiResourceEntityV1alpha1Validator: KindValidator = { }, }; -/** - * Type guard for {@link RuleAiResourceEntityV1alpha1}. - * - * @alpha - */ -export const isRuleAiResourceEntity = ( - entity: Entity, -): entity is RuleAiResourceEntityV1alpha1 => - isAiResourceEntity(entity) && entity.spec?.type === 'rule'; - const baseRelationFields = [ { selector: { path: 'spec.owner' }, From a185d7e0f8b74f1df8cecee5fcc3e33226be386c Mon Sep 17 00:00:00 2001 From: benjdlambert Date: Tue, 19 May 2026 12:14:25 +0200 Subject: [PATCH 14/15] feat: restore per-type guards for union narrowing Signed-off-by: benjdlambert --- packages/catalog-model/report-alpha.api.md | 10 ++++ packages/catalog-model/src/alpha.ts | 2 + .../kinds/AiResourceEntityV1alpha1.test.ts | 56 +++++++++++++++++++ .../src/kinds/AiResourceEntityV1alpha1.ts | 20 +++++++ 4 files changed, 88 insertions(+) diff --git a/packages/catalog-model/report-alpha.api.md b/packages/catalog-model/report-alpha.api.md index 4d264e6793..d1f5e4ef61 100644 --- a/packages/catalog-model/report-alpha.api.md +++ b/packages/catalog-model/report-alpha.api.md @@ -485,6 +485,16 @@ export const isAiResourceEntity: ( entity: Entity, ) => entity is AiResourceEntityV1alpha1; +// @alpha +export const isRuleAiResourceEntity: ( + entity: Entity, +) => entity is RuleAiResourceEntityV1alpha1; + +// @alpha +export const isSkillAiResourceEntity: ( + entity: Entity, +) => entity is SkillAiResourceEntityV1alpha1; + // @public export type KindValidator = { check(entity: Entity): Promise; diff --git a/packages/catalog-model/src/alpha.ts b/packages/catalog-model/src/alpha.ts index 1d8f1c01af..219513f167 100644 --- a/packages/catalog-model/src/alpha.ts +++ b/packages/catalog-model/src/alpha.ts @@ -38,6 +38,8 @@ export { skillAiResourceEntityV1alpha1Validator, ruleAiResourceEntityV1alpha1Validator, isAiResourceEntity, + isSkillAiResourceEntity, + isRuleAiResourceEntity, aiResourceEntityModel, } from './kinds/AiResourceEntityV1alpha1'; export * from './model'; diff --git a/packages/catalog-model/src/kinds/AiResourceEntityV1alpha1.test.ts b/packages/catalog-model/src/kinds/AiResourceEntityV1alpha1.test.ts index 688f25f5c6..be295c81dd 100644 --- a/packages/catalog-model/src/kinds/AiResourceEntityV1alpha1.test.ts +++ b/packages/catalog-model/src/kinds/AiResourceEntityV1alpha1.test.ts @@ -23,6 +23,8 @@ import { skillAiResourceEntityV1alpha1Validator as skillValidator, ruleAiResourceEntityV1alpha1Validator as ruleValidator, isAiResourceEntity, + isSkillAiResourceEntity, + isRuleAiResourceEntity, } from './AiResourceEntityV1alpha1'; describe('AiResourceV1alpha1 default validator', () => { @@ -301,3 +303,57 @@ describe('isAiResourceEntity', () => { expect(isAiResourceEntity(entity)).toBe(false); }); }); + +describe('isSkillAiResourceEntity', () => { + it('returns true for a skill AiResource', () => { + const entity: Entity = { + apiVersion: 'backstage.io/v1alpha1', + kind: 'AiResource', + metadata: { name: 'test' }, + spec: { type: 'skill' }, + }; + expect(isSkillAiResourceEntity(entity)).toBe(true); + }); + + it('returns false for a non-skill AiResource', () => { + const entity: Entity = { + apiVersion: 'backstage.io/v1alpha1', + kind: 'AiResource', + metadata: { name: 'test' }, + spec: { type: 'rule' }, + }; + expect(isSkillAiResourceEntity(entity)).toBe(false); + }); + + it('returns false for a different kind', () => { + const entity: Entity = { + apiVersion: 'backstage.io/v1alpha1', + kind: 'Component', + metadata: { name: 'test' }, + spec: { type: 'skill' }, + }; + expect(isSkillAiResourceEntity(entity)).toBe(false); + }); +}); + +describe('isRuleAiResourceEntity', () => { + it('returns true for a rule AiResource', () => { + const entity: Entity = { + apiVersion: 'backstage.io/v1alpha1', + kind: 'AiResource', + metadata: { name: 'test' }, + spec: { type: 'rule' }, + }; + expect(isRuleAiResourceEntity(entity)).toBe(true); + }); + + it('returns false for a non-rule AiResource', () => { + const entity: Entity = { + apiVersion: 'backstage.io/v1alpha1', + kind: 'AiResource', + metadata: { name: 'test' }, + spec: { type: 'skill' }, + }; + expect(isRuleAiResourceEntity(entity)).toBe(false); + }); +}); diff --git a/packages/catalog-model/src/kinds/AiResourceEntityV1alpha1.ts b/packages/catalog-model/src/kinds/AiResourceEntityV1alpha1.ts index 109712c5ec..8ee9374a45 100644 --- a/packages/catalog-model/src/kinds/AiResourceEntityV1alpha1.ts +++ b/packages/catalog-model/src/kinds/AiResourceEntityV1alpha1.ts @@ -125,6 +125,26 @@ export const isAiResourceEntity = ( ): entity is AiResourceEntityV1alpha1 => entity.apiVersion === 'backstage.io/v1alpha1' && entity.kind === 'AiResource'; +/** + * Type guard for {@link SkillAiResourceEntityV1alpha1}. + * + * @alpha + */ +export const isSkillAiResourceEntity = ( + entity: Entity, +): entity is SkillAiResourceEntityV1alpha1 => + isAiResourceEntity(entity) && entity.spec?.type === 'skill'; + +/** + * Type guard for {@link RuleAiResourceEntityV1alpha1}. + * + * @alpha + */ +export const isRuleAiResourceEntity = ( + entity: Entity, +): entity is RuleAiResourceEntityV1alpha1 => + isAiResourceEntity(entity) && entity.spec?.type === 'rule'; + const ruleValidator = entityKindSchemaValidator(ruleJsonSchema); /** From 19f9330073ed8cbacab748d3e0d56ecd96e1aaa5 Mon Sep 17 00:00:00 2001 From: benjdlambert Date: Tue, 19 May 2026 14:14:30 +0200 Subject: [PATCH 15/15] docs: document source-location annotation for skill/rule content Signed-off-by: benjdlambert --- .../catalog-model/src/kinds/AiResourceEntityV1alpha1.ts | 7 +++++++ plugins/catalog-backend-module-ai-model/README.md | 5 +++++ 2 files changed, 12 insertions(+) diff --git a/packages/catalog-model/src/kinds/AiResourceEntityV1alpha1.ts b/packages/catalog-model/src/kinds/AiResourceEntityV1alpha1.ts index 8ee9374a45..f73c7a1682 100644 --- a/packages/catalog-model/src/kinds/AiResourceEntityV1alpha1.ts +++ b/packages/catalog-model/src/kinds/AiResourceEntityV1alpha1.ts @@ -26,6 +26,13 @@ import ruleJsonSchema from '../schema/kinds/AiResource.v1alpha1.rule.schema.json /** * Default AiResource entity for types that don't have a structured spec. * + * @remarks + * + * The actual content of skills and rules is not stored in the entity spec. + * Instead, the source file is referenced via the standard + * `backstage.io/source-location` annotation, consistent with how other + * Backstage entities reference their source files. + * * @alpha */ export interface AiResourceEntityV1alpha1Default extends Entity { diff --git a/plugins/catalog-backend-module-ai-model/README.md b/plugins/catalog-backend-module-ai-model/README.md index 907c074c83..7b85733ffe 100644 --- a/plugins/catalog-backend-module-ai-model/README.md +++ b/plugins/catalog-backend-module-ai-model/README.md @@ -36,5 +36,10 @@ spec: The `type` field determines which spec fields are available. Currently supported types: - **`skill`** — reusable contextual knowledge for AI coding tools. Supports additional fields: `disciplines`, `categories`, `agents`, `dependsOn`. +- **`rule`** — governance rules and constraints for AI coding tools. Supports additional fields: `disciplines`, `category` (required), `rationale` (required). Any other `type` value is accepted with the base spec fields: `type`, `lifecycle`, `owner`, and optionally `system`. + +## Accessing skill and rule content + +The actual content of skills and rules is not stored in the entity spec. Instead, the source file is referenced via the standard `backstage.io/source-location` annotation, consistent with how other Backstage entities reference their source files. Entity providers that generate `AiResource` entities from `SKILL.md` or rule files should set this annotation to point to the source file.