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 <ben@blam.sh>
This commit is contained in:
@@ -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;
|
||||
```
|
||||
|
||||
+173
-57
@@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
+90
-8
@@ -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,
|
||||
},
|
||||
},
|
||||
],
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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 }] };
|
||||
},
|
||||
});
|
||||
},
|
||||
});
|
||||
},
|
||||
|
||||
+101
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
Reference in New Issue
Block a user