improve postgres best practices and add evals

This commit is contained in:
Pedro Rodrigues
2026-01-23 17:26:45 +00:00
parent bbde7ff5f8
commit 1d9f4ea441
33 changed files with 6024 additions and 11 deletions

View File

@@ -7,7 +7,8 @@ import {
validateSkillExists,
} from "./config.js";
import { parseRuleFile } from "./parser.js";
import type { Metadata, Rule, Section } from "./types.js";
import { filterRulesForProfile, listProfiles, loadProfile } from "./profiles.js";
import type { Metadata, Profile, Rule, Section } from "./types.js";
import { validateRuleFile } from "./validate.js";
/**
@@ -100,8 +101,13 @@ export function generateSectionMap(
/**
* Build AGENTS.md for a specific skill
*/
function buildSkill(paths: SkillPaths): void {
console.log(`[${paths.name}] Building AGENTS.md...`);
function buildSkill(paths: SkillPaths, profile?: Profile): void {
const profileSuffix = profile ? `.${profile.name}` : "";
const outputFile = profile
? paths.agentsOutput.replace(".md", `${profileSuffix}.md`)
: paths.agentsOutput;
console.log(`[${paths.name}] Building AGENTS${profileSuffix}.md...`);
// Load metadata and sections
const metadata = loadMetadata(paths.metadataFile, paths.name);
@@ -113,7 +119,7 @@ function buildSkill(paths: SkillPaths): void {
if (!existsSync(paths.rulesDir)) {
console.log(` No rules directory found. Generating empty AGENTS.md.`);
writeFileSync(
paths.agentsOutput,
outputFile,
`# ${skillTitle}\n\nNo rules defined yet.\n`,
);
return;
@@ -147,10 +153,17 @@ function buildSkill(paths: SkillPaths): void {
}
}
// Filter rules by profile if specified
let filteredRules = rules;
if (profile) {
filteredRules = filterRulesForProfile(rules, profile);
console.log(` Filtered to ${filteredRules.length} rules for profile "${profile.name}"`);
}
// Group rules by section and assign IDs
const rulesBySection = new Map<number, Rule[]>();
for (const rule of rules) {
for (const rule of filteredRules) {
const sectionRules = rulesBySection.get(rule.section) || [];
sectionRules.push(rule);
rulesBySection.set(rule.section, sectionRules);
@@ -225,6 +238,18 @@ function buildSkill(paths: SkillPaths): void {
output.push(`**Impact: ${rule.impact}**\n`);
}
// Add prerequisites if minVersion or extensions are specified
const prerequisites: string[] = [];
if (rule.minVersion) {
prerequisites.push(`PostgreSQL ${rule.minVersion}+`);
}
if (rule.extensions && rule.extensions.length > 0) {
prerequisites.push(`Extension${rule.extensions.length > 1 ? "s" : ""}: ${rule.extensions.join(", ")}`);
}
if (prerequisites.length > 0) {
output.push(`**Prerequisites:** ${prerequisites.join(" | ")}\n`);
}
output.push(`${rule.explanation}\n`);
for (const example of rule.examples) {
@@ -269,9 +294,52 @@ function buildSkill(paths: SkillPaths): void {
}
// Write output
writeFileSync(paths.agentsOutput, output.join("\n"));
console.log(` Generated: ${paths.agentsOutput}`);
console.log(` Total rules: ${rules.length}`);
writeFileSync(outputFile, output.join("\n"));
console.log(` Generated: ${outputFile}`);
console.log(` Total rules: ${filteredRules.length}`);
}
/**
* Parse CLI arguments
*/
function parseArgs(): { skill?: string; profile?: string; allProfiles: boolean } {
const args = process.argv.slice(2);
let skill: string | undefined;
let profile: string | undefined;
let allProfiles = false;
for (let i = 0; i < args.length; i++) {
const arg = args[i];
if (arg === "--profile" && args[i + 1]) {
profile = args[i + 1];
i++;
} else if (arg === "--all-profiles") {
allProfiles = true;
} else if (!arg.startsWith("--")) {
skill = arg;
}
}
return { skill, profile, allProfiles };
}
/**
* Build a skill with all available profiles
*/
function buildSkillWithAllProfiles(paths: SkillPaths): void {
const profilesDir = join(paths.skillDir, "profiles");
const profiles = listProfiles(profilesDir);
// Build default (no profile)
buildSkill(paths);
// Build each profile variant
for (const profileName of profiles) {
const profile = loadProfile(profilesDir, profileName);
if (profile) {
buildSkill(paths, profile);
}
}
}
// Run build when executed directly
@@ -280,7 +348,7 @@ const isMainModule =
process.argv[1]?.endsWith("build.js");
if (isMainModule) {
const targetSkill = process.argv[2];
const { skill: targetSkill, profile: profileName, allProfiles } = parseArgs();
if (targetSkill) {
// Build specific skill
@@ -292,7 +360,29 @@ if (isMainModule) {
}
process.exit(1);
}
buildSkill(getSkillPaths(targetSkill));
const paths = getSkillPaths(targetSkill);
if (allProfiles) {
// Build all profile variants
buildSkillWithAllProfiles(paths);
} else if (profileName) {
// Build with specific profile
const profilesDir = join(paths.skillDir, "profiles");
const profile = loadProfile(profilesDir, profileName);
if (!profile) {
console.error(`Error: Profile "${profileName}" not found`);
const available = listProfiles(profilesDir);
if (available.length > 0) {
console.error(`Available profiles: ${available.join(", ")}`);
}
process.exit(1);
}
buildSkill(paths, profile);
} else {
// Build default
buildSkill(paths);
}
} else {
// Build all skills
const skills = discoverSkills();
@@ -303,7 +393,12 @@ if (isMainModule) {
console.log(`Found ${skills.length} skill(s): ${skills.join(", ")}\n`);
for (const skill of skills) {
buildSkill(getSkillPaths(skill));
const paths = getSkillPaths(skill);
if (allProfiles) {
buildSkillWithAllProfiles(paths);
} else {
buildSkill(paths);
}
console.log("");
}
}

View File

@@ -251,6 +251,7 @@ export function parseRuleFile(
const examples = extractExamples(body);
const tags = frontmatter.tags?.split(",").map((t) => t.trim()) || [];
const extensions = frontmatter.extensions?.split(",").map((e) => e.trim()) || [];
// Validation warnings
if (!explanation || explanation.length < 20) {
@@ -271,6 +272,8 @@ export function parseRuleFile(
examples,
references: extractReferences(body),
tags: tags.length > 0 ? tags : undefined,
minVersion: frontmatter.minVersion || undefined,
extensions: extensions.length > 0 ? extensions : undefined,
};
return { success: true, rule, errors, warnings };

View File

@@ -0,0 +1,102 @@
import { existsSync, readdirSync, readFileSync } from "node:fs";
import { join } from "node:path";
import type { Profile, Rule } from "./types.js";
/**
* Load a profile from the profiles directory
*/
export function loadProfile(profilesDir: string, profileName: string): Profile | null {
const profileFile = join(profilesDir, `${profileName}.json`);
if (!existsSync(profileFile)) {
return null;
}
try {
return JSON.parse(readFileSync(profileFile, "utf-8"));
} catch (error) {
console.error(`Error loading profile ${profileName}:`, error);
return null;
}
}
/**
* List all available profiles in the profiles directory
*/
export function listProfiles(profilesDir: string): string[] {
if (!existsSync(profilesDir)) {
return [];
}
return readdirSync(profilesDir)
.filter((f) => f.endsWith(".json"))
.map((f) => f.replace(".json", ""));
}
/**
* Compare version strings (e.g., "9.5", "11", "14.2")
* Returns: negative if a < b, 0 if equal, positive if a > b
*/
function compareVersions(a: string, b: string): number {
const partsA = a.split(".").map(Number);
const partsB = b.split(".").map(Number);
const maxLen = Math.max(partsA.length, partsB.length);
for (let i = 0; i < maxLen; i++) {
const numA = partsA[i] || 0;
const numB = partsB[i] || 0;
if (numA !== numB) {
return numA - numB;
}
}
return 0;
}
/**
* Check if a rule is compatible with a profile
*/
export function isRuleCompatibleWithProfile(rule: Rule, profile: Profile): boolean {
// Check version requirement
if (rule.minVersion) {
if (compareVersions(rule.minVersion, profile.minVersion) > 0) {
// Rule requires a higher version than profile supports
return false;
}
if (profile.maxVersion && compareVersions(rule.minVersion, profile.maxVersion) > 0) {
// Rule requires a version higher than profile's max
return false;
}
}
// Check extension requirements
if (rule.extensions && rule.extensions.length > 0) {
const allExtensions = [
...(profile.extensions.available || []),
...(profile.extensions.installable || []),
];
for (const ext of rule.extensions) {
if (profile.extensions.unavailable?.includes(ext)) {
// Extension is explicitly unavailable in this profile
return false;
}
if (!allExtensions.includes(ext)) {
// Extension is not available or installable
return false;
}
}
}
// Check if rule is explicitly excluded
if (profile.excludeRules?.includes(rule.id)) {
return false;
}
return true;
}
/**
* Filter rules based on profile constraints
*/
export function filterRulesForProfile(rules: Rule[], profile: Profile): Rule[] {
return rules.filter((rule) => isRuleCompatibleWithProfile(rule, profile));
}

View File

@@ -26,6 +26,8 @@ export interface Rule {
references?: string[];
tags?: string[];
supabaseNotes?: string;
minVersion?: string; // Minimum PostgreSQL version required (e.g., "11", "14")
extensions?: string[]; // Required PostgreSQL extensions (e.g., ["pg_stat_statements"])
}
export interface Section {
@@ -57,3 +59,16 @@ export interface ValidationResult {
errors: string[];
warnings: string[];
}
export interface Profile {
name: string;
minVersion: string;
maxVersion?: string;
extensions: {
available: string[];
installable?: string[];
unavailable: string[];
};
excludeRules?: string[];
notes?: string;
}