mirror of
https://github.com/forcedotcom/afv-library.git
synced 2026-08-10 17:41:49 +08:00
implement checks based on jeff's best practices doc
This commit is contained in:
parent
d7d3ca352a
commit
b7712ce3cc
@ -27,6 +27,14 @@ interface CheckResult {
|
|||||||
errors: string[]
|
errors: string[]
|
||||||
/** When true and errors is non-empty, skip remaining checks for this entry. */
|
/** When true and errors is non-empty, skip remaining checks for this entry. */
|
||||||
fatal?: boolean
|
fatal?: boolean
|
||||||
|
/** Defaults to "error". Warnings are printed but do not cause a non-zero exit code. */
|
||||||
|
severity?: "error" | "warning"
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Collected results for a single skill entry. */
|
||||||
|
interface SkillResult {
|
||||||
|
errors: string[]
|
||||||
|
warnings: string[]
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Runs before SKILL.md is read — validates directory layout. */
|
/** Runs before SKILL.md is read — validates directory layout. */
|
||||||
@ -64,6 +72,27 @@ const STRUCTURE_CHECKS: StructureCheck[] = [
|
|||||||
return { errors: [] }
|
return { errors: [] }
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
description: "Name must be kebab-case (lowercase letters, digits, and hyphens only)",
|
||||||
|
run(dirName) {
|
||||||
|
if (!/^[a-z0-9]+(-[a-z0-9]+)*$/.test(dirName)) {
|
||||||
|
return { errors: [`skills/${dirName}: name must be kebab-case (only lowercase letters, digits, and hyphens)`] }
|
||||||
|
}
|
||||||
|
return { errors: [] }
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
description: "Name should use gerund form — first word should end in -ing (e.g. generating-apex-tests)",
|
||||||
|
run(dirName) {
|
||||||
|
if (!dirName.split("-")[0].endsWith("ing")) {
|
||||||
|
return {
|
||||||
|
errors: [`skills/${dirName}: name should use gerund form (e.g. generating-apex-tests, refactoring-triggers)`],
|
||||||
|
severity: "warning",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return { errors: [] }
|
||||||
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
description: "Skills must be exactly one level deep (no nested category directories)",
|
description: "Skills must be exactly one level deep (no nested category directories)",
|
||||||
run(dirName, dirPath) {
|
run(dirName, dirPath) {
|
||||||
@ -134,6 +163,46 @@ const CONTENT_CHECKS: ContentCheck[] = [
|
|||||||
return { errors: [] }
|
return { errors: [] }
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
description: "Description must be at least 20 words to be information-rich",
|
||||||
|
run({ dirName, frontmatter }) {
|
||||||
|
if (!frontmatter) return { errors: [] }
|
||||||
|
const words = frontmatter.description?.trim().split(/\s+/) ?? []
|
||||||
|
if (words.length < 20) {
|
||||||
|
return {
|
||||||
|
errors: [`skills/${dirName}/SKILL.md: description too short (${words.length} word(s), minimum 20)`],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return { errors: [] }
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
description: 'Description must include trigger/activation language (contain "use")',
|
||||||
|
run({ dirName, frontmatter }) {
|
||||||
|
if (!frontmatter) return { errors: [] }
|
||||||
|
if (!frontmatter.description?.toLowerCase().includes("use")) {
|
||||||
|
return {
|
||||||
|
errors: [
|
||||||
|
`skills/${dirName}/SKILL.md: description must include trigger context (e.g. "Use this skill when...")`,
|
||||||
|
],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return { errors: [] }
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
description: "Skill body should be under 500 lines for context efficiency",
|
||||||
|
run({ dirName, body }) {
|
||||||
|
const lines = body.split("\n").length
|
||||||
|
if (lines > 500) {
|
||||||
|
return {
|
||||||
|
errors: [`skills/${dirName}/SKILL.md: body is ${lines} lines (recommended maximum is 500)`],
|
||||||
|
severity: "warning",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return { errors: [] }
|
||||||
|
},
|
||||||
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -181,19 +250,24 @@ function getFrontmatterEnd(content: string): number {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Runs all structure and content checks for a single skill directory.
|
* Runs all structure and content checks for a single skill directory.
|
||||||
* Returns an array of error messages (empty = pass).
|
* Returns errors (block CI) and warnings (advisory, printed but exit 0) separately.
|
||||||
*/
|
*/
|
||||||
function validateSkill(dirName: string, dirPath: string): string[] {
|
function validateSkill(dirName: string, dirPath: string): SkillResult {
|
||||||
if (!fs.existsSync(dirPath)) {
|
if (!fs.existsSync(dirPath)) {
|
||||||
return [`skills/${dirName}: directory not found`]
|
return { errors: [`skills/${dirName}: directory not found`], warnings: [] }
|
||||||
}
|
}
|
||||||
|
|
||||||
const errors: string[] = []
|
const errors: string[] = []
|
||||||
|
const warnings: string[] = []
|
||||||
|
|
||||||
|
const collect = ({ errors: msgs, fatal, severity }: CheckResult): boolean => {
|
||||||
|
if (msgs.length === 0) return false
|
||||||
|
;(severity === "warning" ? warnings : errors).push(...msgs)
|
||||||
|
return !!fatal && severity !== "warning"
|
||||||
|
}
|
||||||
|
|
||||||
for (const check of STRUCTURE_CHECKS) {
|
for (const check of STRUCTURE_CHECKS) {
|
||||||
const { errors: checkErrors, fatal } = check.run(dirName, dirPath)
|
if (collect(check.run(dirName, dirPath))) return { errors, warnings }
|
||||||
errors.push(...checkErrors)
|
|
||||||
if (fatal && checkErrors.length > 0) return errors
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const content = fs.readFileSync(path.join(dirPath, "SKILL.md"), "utf8")
|
const content = fs.readFileSync(path.join(dirPath, "SKILL.md"), "utf8")
|
||||||
@ -203,12 +277,10 @@ function validateSkill(dirName: string, dirPath: string): string[] {
|
|||||||
const ctx: SkillContext = { dirName, dirPath, content, frontmatter, body }
|
const ctx: SkillContext = { dirName, dirPath, content, frontmatter, body }
|
||||||
|
|
||||||
for (const check of CONTENT_CHECKS) {
|
for (const check of CONTENT_CHECKS) {
|
||||||
const { errors: checkErrors, fatal } = check.run(ctx)
|
if (collect(check.run(ctx))) return { errors, warnings }
|
||||||
errors.push(...checkErrors)
|
|
||||||
if (fatal && checkErrors.length > 0) return errors
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return errors
|
return { errors, warnings }
|
||||||
}
|
}
|
||||||
|
|
||||||
/** CLI entry point. Parses flags, resolves the list of skills to check, and reports results. */
|
/** CLI entry point. Parses flags, resolves the list of skills to check, and reports results. */
|
||||||
@ -237,12 +309,22 @@ function main(): void {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const allErrors: string[] = []
|
const allErrors: string[] = []
|
||||||
|
const allWarnings: string[] = []
|
||||||
let passed = 0
|
let passed = 0
|
||||||
|
|
||||||
for (const entry of entries) {
|
for (const entry of entries) {
|
||||||
const entryErrors = validateSkill(entry, path.join(SKILLS_DIR, entry))
|
const { errors, warnings } = validateSkill(entry, path.join(SKILLS_DIR, entry))
|
||||||
allErrors.push(...entryErrors)
|
allErrors.push(...errors)
|
||||||
if (entryErrors.length === 0) passed++
|
allWarnings.push(...warnings)
|
||||||
|
if (errors.length === 0) passed++
|
||||||
|
}
|
||||||
|
|
||||||
|
if (allWarnings.length > 0) {
|
||||||
|
console.warn(`\n${allWarnings.length} warning(s):\n`)
|
||||||
|
for (const w of allWarnings) {
|
||||||
|
console.warn(` ⚠ ${w}`)
|
||||||
|
}
|
||||||
|
console.warn("")
|
||||||
}
|
}
|
||||||
|
|
||||||
if (allErrors.length > 0) {
|
if (allErrors.length > 0) {
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user