Externalize audit pipeline services
This commit is contained in:
@@ -13,20 +13,27 @@ export const SKILL_CATEGORIES = [
|
||||
export type SkillCategory = (typeof SKILL_CATEGORIES)[number];
|
||||
|
||||
export type SkillRegistryEntry = {
|
||||
id?: string;
|
||||
name: string;
|
||||
title?: string;
|
||||
purpose: string;
|
||||
whenToUse: string;
|
||||
whenNotToUse: string;
|
||||
requiredInput: string;
|
||||
expectedOutput: string;
|
||||
category: SkillCategory;
|
||||
category?: SkillCategory;
|
||||
appliesWhen?: string;
|
||||
inputs?: string[];
|
||||
outputs?: string;
|
||||
instructions?: string;
|
||||
version?: string;
|
||||
source?: string;
|
||||
};
|
||||
|
||||
export type AuditUsedSkill = {
|
||||
id?: string;
|
||||
name: string;
|
||||
category: SkillCategory;
|
||||
category?: SkillCategory;
|
||||
version?: string;
|
||||
source?: string;
|
||||
};
|
||||
@@ -51,6 +58,7 @@ const REQUIRED_FIELDS: ParsedFieldName[] = [
|
||||
];
|
||||
|
||||
const FIELD_LABELS_RE = /^(Purpose|When to use|When not to use|Required input|Expected output|Category|Version|Source):\s*(.*?)\s*$/;
|
||||
const V3_META_BLOCK_RE = /```yaml\s*\n([\s\S]*?)\n```\s*\n?([\s\S]*)$/;
|
||||
|
||||
function normalizeCategory(value: string): SkillCategory {
|
||||
const normalized = value.toLowerCase();
|
||||
@@ -129,6 +137,108 @@ function parseSection(lines: string[], sectionIndex: number): SkillRegistryEntry
|
||||
};
|
||||
}
|
||||
|
||||
function parseV3List(value: string): string[] {
|
||||
const trimmed = value.trim();
|
||||
if (!trimmed.startsWith("[") || !trimmed.endsWith("]")) {
|
||||
return trimmed ? [trimmed] : [];
|
||||
}
|
||||
|
||||
return trimmed
|
||||
.slice(1, -1)
|
||||
.split(",")
|
||||
.map((entry) => entry.trim())
|
||||
.filter(Boolean);
|
||||
}
|
||||
|
||||
function parseV3MetaBlock(metaSource: string): Record<string, string> {
|
||||
const values: Record<string, string> = {};
|
||||
|
||||
for (const line of metaSource.split("\n")) {
|
||||
const match = line.trim().match(/^([a-z_]+):\s*(.*?)\s*$/);
|
||||
if (match) {
|
||||
values[match[1]] = match[2].trim();
|
||||
}
|
||||
}
|
||||
|
||||
return values;
|
||||
}
|
||||
|
||||
function parseV3Section(
|
||||
rawBody: string,
|
||||
sectionIndex: number,
|
||||
): SkillRegistryEntry | null {
|
||||
const match = rawBody.match(V3_META_BLOCK_RE);
|
||||
if (!match) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const values = parseV3MetaBlock(match[1]);
|
||||
if (!values.id) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const requiredFields = ["id", "title", "applies_when", "inputs", "outputs"];
|
||||
for (const field of requiredFields) {
|
||||
if (!values[field]) {
|
||||
throw new Error(
|
||||
`Missing required v3 field "${field}" for skill section ${sectionIndex}.`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const id = values.id;
|
||||
const title = values.title;
|
||||
const inputs = parseV3List(values.inputs);
|
||||
const instructions = match[2].trim();
|
||||
|
||||
if (instructions.length === 0) {
|
||||
throw new Error(`Missing instructions for v3 skill "${id}".`);
|
||||
}
|
||||
|
||||
return {
|
||||
id,
|
||||
name: title,
|
||||
title,
|
||||
purpose: instructions,
|
||||
whenToUse: values.applies_when,
|
||||
whenNotToUse: "Use only when applies_when and inputs match.",
|
||||
requiredInput: inputs.join(", "),
|
||||
expectedOutput: values.outputs,
|
||||
appliesWhen: values.applies_when,
|
||||
inputs,
|
||||
outputs: values.outputs,
|
||||
instructions,
|
||||
};
|
||||
}
|
||||
|
||||
function addParsedEntry(
|
||||
entries: SkillRegistryEntry[],
|
||||
names: Set<string>,
|
||||
ids: Set<string>,
|
||||
parsed: SkillRegistryEntry,
|
||||
) {
|
||||
const normalizedName = parsed.name.trim().toLowerCase();
|
||||
if (names.has(normalizedName)) {
|
||||
throw new Error(`Duplicate skill name "${parsed.name}" in skills registry.`);
|
||||
}
|
||||
if (parsed.id) {
|
||||
const normalizedId = parsed.id.trim().toLowerCase();
|
||||
if (ids.has(normalizedId)) {
|
||||
throw new Error(`Duplicate skill id "${parsed.id}" in skills registry.`);
|
||||
}
|
||||
ids.add(normalizedId);
|
||||
}
|
||||
|
||||
names.add(normalizedName);
|
||||
entries.push(parsed);
|
||||
}
|
||||
|
||||
function hasLegacyFieldLabels(source: string): boolean {
|
||||
return source
|
||||
.split("\n")
|
||||
.some((line) => FIELD_LABELS_RE.test(line.trim()));
|
||||
}
|
||||
|
||||
export function parseSkillsRegistry(source: string): SkillRegistryEntry[] {
|
||||
const normalized = source.replace(/\r\n/g, "\n");
|
||||
const rawSections = normalized
|
||||
@@ -138,6 +248,45 @@ export function parseSkillsRegistry(source: string): SkillRegistryEntry[] {
|
||||
|
||||
const entries: SkillRegistryEntry[] = [];
|
||||
const names = new Set<string>();
|
||||
const ids = new Set<string>();
|
||||
const v3Entries: SkillRegistryEntry[] = [];
|
||||
|
||||
for (let index = 0; index < rawSections.length; index += 1) {
|
||||
const rawSection = rawSections[index];
|
||||
const lines = rawSection
|
||||
.split("\n")
|
||||
.map((line) => line.trimEnd())
|
||||
.filter((line, lineIndex) => line.length > 0 || lineIndex === 0);
|
||||
const sectionBody = lines.slice(1).join("\n");
|
||||
const parsed = parseV3Section(sectionBody, index + 1);
|
||||
if (parsed && parsed.id !== "kebab-case-id") {
|
||||
v3Entries.push(parsed);
|
||||
}
|
||||
}
|
||||
|
||||
if (v3Entries.length > 0) {
|
||||
for (let index = 0; index < rawSections.length; index += 1) {
|
||||
const rawSection = rawSections[index];
|
||||
const lines = rawSection
|
||||
.split("\n")
|
||||
.map((line) => line.trimEnd())
|
||||
.filter((line, lineIndex) => line.length > 0 || lineIndex === 0);
|
||||
const sectionTitle = lines.at(0) ?? "";
|
||||
const sectionBody = lines.slice(1).join("\n");
|
||||
const sectionLines = [`## ${sectionTitle}`, ...lines.slice(1)];
|
||||
const parsed = parseV3Section(sectionBody, index + 1);
|
||||
if (parsed) {
|
||||
if (parsed.id !== "kebab-case-id") {
|
||||
addParsedEntry(entries, names, ids, parsed);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (hasLegacyFieldLabels(sectionBody)) {
|
||||
addParsedEntry(entries, names, ids, parseSection(sectionLines, index + 1));
|
||||
}
|
||||
}
|
||||
return entries;
|
||||
}
|
||||
|
||||
for (let index = 0; index < rawSections.length; index += 1) {
|
||||
const rawSection = rawSections[index];
|
||||
@@ -146,16 +295,10 @@ export function parseSkillsRegistry(source: string): SkillRegistryEntry[] {
|
||||
.map((line) => line.trimEnd())
|
||||
.filter((line, lineIndex) => line.length > 0 || lineIndex === 0);
|
||||
|
||||
const sectionLines = [`## ${lines.at(0) ?? ""}`, ...lines.slice(1)];
|
||||
const sectionTitle = lines.at(0) ?? "";
|
||||
const sectionLines = [`## ${sectionTitle}`, ...lines.slice(1)];
|
||||
const parsed = parseSection(sectionLines, index + 1);
|
||||
|
||||
const normalizedName = parsed.name.trim().toLowerCase();
|
||||
if (names.has(normalizedName)) {
|
||||
throw new Error(`Duplicate skill name "${parsed.name}" in skills registry.`);
|
||||
}
|
||||
|
||||
names.add(normalizedName);
|
||||
entries.push(parsed);
|
||||
addParsedEntry(entries, names, ids, parsed);
|
||||
}
|
||||
|
||||
return entries;
|
||||
@@ -169,10 +312,24 @@ export async function loadSkillsRegistry(
|
||||
}
|
||||
|
||||
export function toAuditUsedSkill(skill: SkillRegistryEntry): AuditUsedSkill {
|
||||
return {
|
||||
const usedSkill: AuditUsedSkill = {
|
||||
name: skill.name,
|
||||
category: skill.category,
|
||||
version: skill.version,
|
||||
source: skill.source,
|
||||
};
|
||||
|
||||
if (skill.id) {
|
||||
usedSkill.id = skill.id;
|
||||
}
|
||||
if (skill.category) {
|
||||
usedSkill.category = skill.category;
|
||||
}
|
||||
if (!skill.version) {
|
||||
delete usedSkill.version;
|
||||
}
|
||||
if (!skill.source) {
|
||||
delete usedSkill.source;
|
||||
}
|
||||
|
||||
return usedSkill;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user