mirror of
https://github.com/katanemo/plano.git
synced 2026-06-08 14:55:14 +02:00
Some checks are pending
CI / pre-commit (push) Waiting to run
CI / plano-tools-tests (push) Waiting to run
CI / native-smoke-test (push) Waiting to run
CI / docker-build (push) Waiting to run
CI / validate-config (push) Waiting to run
CI / security-scan (push) Blocked by required conditions
CI / test-prompt-gateway (push) Blocked by required conditions
CI / test-model-alias-routing (push) Blocked by required conditions
CI / test-responses-api-with-state (push) Blocked by required conditions
CI / e2e-plano-tests (3.10) (push) Blocked by required conditions
CI / e2e-plano-tests (3.11) (push) Blocked by required conditions
CI / e2e-plano-tests (3.12) (push) Blocked by required conditions
CI / e2e-plano-tests (3.13) (push) Blocked by required conditions
CI / e2e-plano-tests (3.14) (push) Blocked by required conditions
CI / e2e-demo-preference (push) Blocked by required conditions
CI / e2e-demo-currency (push) Blocked by required conditions
Publish docker image (latest) / build-arm64 (push) Waiting to run
Publish docker image (latest) / build-amd64 (push) Waiting to run
Publish docker image (latest) / create-manifest (push) Blocked by required conditions
Build and Deploy Documentation / build (push) Waiting to run
* feat: add initial documentation for Plano Agent Skills * feat: readme with examples * feat: add detailed skills documentation and examples for Plano --------- Co-authored-by: Adil Hafeez <adil.hafeez@gmail.com>
156 lines
4 KiB
JavaScript
156 lines
4 KiB
JavaScript
#!/usr/bin/env node
|
|
|
|
import { readFileSync, readdirSync } from "node:fs";
|
|
import { join, dirname } from "node:path";
|
|
import { fileURLToPath } from "node:url";
|
|
|
|
type ParsedFrontmatter = {
|
|
frontmatter: Record<string, string>;
|
|
body: string;
|
|
};
|
|
|
|
type ValidationResult = {
|
|
errors: string[];
|
|
warnings: string[];
|
|
};
|
|
|
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
const RULES_DIR = join(__dirname, "..", "rules");
|
|
|
|
const VALID_IMPACTS = [
|
|
"CRITICAL",
|
|
"HIGH",
|
|
"MEDIUM-HIGH",
|
|
"MEDIUM",
|
|
"LOW-MEDIUM",
|
|
"LOW",
|
|
] as const;
|
|
|
|
const SECTION_PREFIXES = [
|
|
"config-",
|
|
"routing-",
|
|
"agent-",
|
|
"filter-",
|
|
"observe-",
|
|
"cli-",
|
|
"deploy-",
|
|
"advanced-",
|
|
];
|
|
|
|
function parseFrontmatter(content: string): ParsedFrontmatter | null {
|
|
const match = content.match(/^---\n([\s\S]*?)\n---\n([\s\S]*)$/);
|
|
if (!match) return null;
|
|
|
|
const frontmatter: Record<string, string> = {};
|
|
const lines = match[1].split("\n");
|
|
for (const line of lines) {
|
|
const colonIdx = line.indexOf(":");
|
|
if (colonIdx === -1) continue;
|
|
const key = line.slice(0, colonIdx).trim();
|
|
const value = line.slice(colonIdx + 1).trim();
|
|
frontmatter[key] = value;
|
|
}
|
|
|
|
return { frontmatter, body: match[2].trim() };
|
|
}
|
|
|
|
function validateFile(file: string, content: string): ValidationResult {
|
|
const errors: string[] = [];
|
|
const warnings: string[] = [];
|
|
|
|
const parsed = parseFrontmatter(content);
|
|
if (!parsed) {
|
|
errors.push("Missing or malformed frontmatter (expected --- ... ---)");
|
|
return { errors, warnings };
|
|
}
|
|
|
|
const { frontmatter, body } = parsed;
|
|
|
|
if (!frontmatter.title) {
|
|
errors.push("Missing required frontmatter field: title");
|
|
}
|
|
if (!frontmatter.impact) {
|
|
errors.push("Missing required frontmatter field: impact");
|
|
} else if (!VALID_IMPACTS.includes(frontmatter.impact as (typeof VALID_IMPACTS)[number])) {
|
|
errors.push(
|
|
`Invalid impact value: "${frontmatter.impact}". Valid values: ${VALID_IMPACTS.join(", ")}`
|
|
);
|
|
}
|
|
if (!frontmatter.tags) {
|
|
warnings.push("No tags defined — consider adding relevant tags");
|
|
}
|
|
|
|
const hasValidPrefix = SECTION_PREFIXES.some((p) => file.startsWith(p));
|
|
if (!hasValidPrefix) {
|
|
errors.push(
|
|
`Filename must start with a valid prefix: ${SECTION_PREFIXES.join(", ")}`
|
|
);
|
|
}
|
|
|
|
if (body.length < 100) {
|
|
warnings.push("Rule body seems very short — consider adding more detail");
|
|
}
|
|
|
|
if (!body.includes("```")) {
|
|
warnings.push(
|
|
"No code examples found — rules should include YAML or CLI examples"
|
|
);
|
|
}
|
|
|
|
if (!body.includes("Incorrect") || !body.includes("Correct")) {
|
|
warnings.push(
|
|
"Consider adding both Incorrect and Correct examples for clarity"
|
|
);
|
|
}
|
|
|
|
return { errors, warnings };
|
|
}
|
|
|
|
function main(): void {
|
|
const files = readdirSync(RULES_DIR)
|
|
.filter((f) => f.endsWith(".md") && !f.startsWith("_"))
|
|
.sort();
|
|
|
|
let totalErrors = 0;
|
|
let totalWarnings = 0;
|
|
let filesWithIssues = 0;
|
|
|
|
console.log(`Validating ${files.length} rule files...\n`);
|
|
|
|
for (const file of files) {
|
|
const content = readFileSync(join(RULES_DIR, file), "utf-8");
|
|
const { errors, warnings } = validateFile(file, content);
|
|
|
|
if (errors.length > 0 || warnings.length > 0) {
|
|
filesWithIssues++;
|
|
console.log(`📄 ${file}`);
|
|
|
|
for (const error of errors) {
|
|
console.log(` ❌ ERROR: ${error}`);
|
|
totalErrors++;
|
|
}
|
|
for (const warning of warnings) {
|
|
console.log(` ⚠️ WARN: ${warning}`);
|
|
totalWarnings++;
|
|
}
|
|
console.log();
|
|
} else {
|
|
console.log(`✅ ${file}`);
|
|
}
|
|
}
|
|
|
|
console.log(`\n--- Validation Summary ---`);
|
|
console.log(`Files checked: ${files.length}`);
|
|
console.log(`Files with issues: ${filesWithIssues}`);
|
|
console.log(`Errors: ${totalErrors}`);
|
|
console.log(`Warnings: ${totalWarnings}`);
|
|
|
|
if (totalErrors > 0) {
|
|
console.log(`\nValidation FAILED with ${totalErrors} error(s).`);
|
|
process.exit(1);
|
|
} else {
|
|
console.log(`\nValidation passed.`);
|
|
}
|
|
}
|
|
|
|
main();
|