aiwg
Version:
Deployment tool and support utility for AI context. Copies agents, skills, commands, rules, and behaviors into the paths each AI platform reads (Claude Code, Codex, Copilot, Cursor, Warp, OpenClaw, and 6 more) so one source of truth works across 10 platfo
157 lines • 5.59 kB
JavaScript
/**
* Pattern-based Quality Scoring Engine
*
* Scores SDLC artifacts against pattern definitions (required/recommended/antipattern).
* Scoring: required (60%) + recommended (40%) - antipattern penalty.
* Thresholds: excellent (90+), good (75+), acceptable (60+), needs-work (<60).
*
* @module quality/scoring
* @issue #192
*/
import { promises as fs } from 'fs';
import path from 'path';
import { fileURLToPath } from 'url';
// ============================================================================
// Constants
// ============================================================================
export const REQUIRED_WEIGHT = 0.60;
export const RECOMMENDED_WEIGHT = 0.40;
export const THRESHOLDS = {
excellent: 90,
good: 75,
acceptable: 60,
};
// ============================================================================
// Scoring Engine
// ============================================================================
export function scoreContent(content, patterns, artifactPath = '') {
const requiredMatches = patterns.required.map((rule) => matchPattern(content, rule));
const requiredFound = requiredMatches.filter((m) => m.found).length;
const requiredScore = patterns.required.length > 0
? (requiredFound / patterns.required.length) * 100
: 100;
const recommendedMatches = patterns.recommended.map((rule) => matchPattern(content, rule));
const recommendedFound = recommendedMatches.filter((m) => m.found).length;
const recommendedScore = patterns.recommended.length > 0
? (recommendedFound / patterns.recommended.length) * 100
: 100;
const antipatternMatches = patterns.antipatterns.map((rule) => matchPattern(content, rule));
const antipatternPenalty = antipatternMatches.reduce((penalty, match) => {
if (match.found) {
const weight = match.rule.weight || 0.05;
return penalty + weight * 100;
}
return penalty;
}, 0);
const rawScore = requiredScore * REQUIRED_WEIGHT +
recommendedScore * RECOMMENDED_WEIGHT -
antipatternPenalty;
const score = Math.max(0, Math.min(100, Math.round(rawScore * 100) / 100));
const grade = getGrade(score);
return {
score,
grade,
breakdown: {
required: Math.round(requiredScore * 100) / 100,
recommended: Math.round(recommendedScore * 100) / 100,
antipatternPenalty: Math.round(antipatternPenalty * 100) / 100,
},
matches: {
required: requiredMatches,
recommended: recommendedMatches,
antipatterns: antipatternMatches,
},
patternId: patterns.id,
artifactPath,
};
}
export function matchPattern(content, rule) {
try {
let pattern = rule.pattern;
let flags = 'gm';
// Handle PCRE inline flag (?i) — strip it and add 'i' to RegExp flags
if (pattern.startsWith('(?i)')) {
pattern = pattern.slice(4);
flags = 'gmi';
}
const regex = new RegExp(pattern, flags);
const matches = content.match(regex);
return {
rule,
found: matches !== null && matches.length > 0,
matchCount: matches ? matches.length : 0,
};
}
catch {
return {
rule,
found: false,
matchCount: 0,
};
}
}
export function getGrade(score) {
if (score >= THRESHOLDS.excellent)
return 'excellent';
if (score >= THRESHOLDS.good)
return 'good';
if (score >= THRESHOLDS.acceptable)
return 'acceptable';
return 'needs-work';
}
// ============================================================================
// Pattern Loading
// ============================================================================
export async function loadBuiltinPattern(artifactType) {
try {
const currentDir = path.dirname(fileURLToPath(import.meta.url));
const patternPath = path.join(currentDir, 'patterns', `${artifactType}.json`);
const content = await fs.readFile(patternPath, 'utf-8');
return JSON.parse(content);
}
catch {
return null;
}
}
export async function loadPatternFromFile(filePath) {
try {
const content = await fs.readFile(filePath, 'utf-8');
return JSON.parse(content);
}
catch {
return null;
}
}
export function getAvailablePatternTypes() {
return ['use-case', 'adr', 'test-plan', 'sad'];
}
export function detectArtifactType(content, filePath) {
const lower = content.toLowerCase();
const basename = path.basename(filePath).toLowerCase();
if (basename.startsWith('uc-') || /^#\s+UC-\d+/m.test(content)) {
return 'use-case';
}
if (basename.startsWith('adr-') || /^#\s+ADR-\d+/m.test(content)) {
return 'adr';
}
if (lower.includes('test plan') || lower.includes('test strategy')) {
return 'test-plan';
}
if (lower.includes('software architecture') ||
lower.includes('system architecture') ||
basename.includes('sad')) {
return 'sad';
}
return null;
}
export async function scoreArtifact(filePath, patternType) {
const content = await fs.readFile(filePath, 'utf-8');
const type = patternType || detectArtifactType(content, filePath);
if (!type)
return null;
const patterns = await loadBuiltinPattern(type);
if (!patterns)
return null;
return scoreContent(content, patterns, filePath);
}
//# sourceMappingURL=scoring.js.map