feat: add rule specType with disciplines, category, and rationale
Signed-off-by: benjdlambert <ben@blam.sh>
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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<boolean>;
|
||||
};
|
||||
|
||||
// @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)
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
},
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
Reference in New Issue
Block a user