Files
16gagent/research/pr-37/pr-37-domain-intelligence/domain-validator.mjs
T
2026-06-06 10:40:48 +08:00

291 lines
10 KiB
JavaScript

/**
* PR-37 Part E — Domain Validator
*
* Validates a requirement package (epics, features, userStories) against
* a domain from the registry, producing coverage, consistency, and
* completeness scores, plus actionable recommendations.
*
* @module pr-37-domain-intelligence/domain-validator
* @since PR-37
*/
import { getDomain, listDomains } from "./domain-registry.mjs";
// ══════════════════════════════════════════════════════════
// Types / typedefs (JSDoc only)
// ══════════════════════════════════════════════════════════
/**
* @typedef {object} ValidationReport
* @property {number} coverageScore - 0-100: % of domain epics covered by generated epics
* @property {number} consistencyScore - 0-100: % of features matching domain expectations
* @property {number} completenessScore - 0-100: % of domain coreEpics present
* @property {string[]} missingEpics - Domain epics NOT present in generated set
* @property {string[]} extraEpics - Generated epics NOT found in domain
* @property {string[]} recommendations - Actionable suggestions
* @property {"pass"|"conditional"|"fail"} overallVerdict - Final verdict
*/
// ══════════════════════════════════════════════════════════
// Internal helpers
// ══════════════════════════════════════════════════════════
/**
* Normalize text for fuzzy comparison.
*
* @param {string} text
* @returns {string}
*/
function normalize(text) {
return text.trim().toLowerCase().replace(/[\s\u3000]+/g, "");
}
/**
* Check if a generated epic title matches a domain epic title.
*
* @param {string} generatedTitle - Title from generated epics
* @param {object[]} domainEpics - Domain coreEpics array
* @returns {boolean}
*/
function matchesDomainEpic(generatedTitle, domainEpics) {
const g = normalize(generatedTitle);
return domainEpics.some(
(de) =>
normalize(de.title) === g ||
normalize(de.title).includes(g) ||
g.includes(normalize(de.title))
);
}
/**
* Check if a generated feature title/name matches a domain feature name.
*
* @param {string} generatedFeatureTitle - Generated feature name
* @param {object} domainFeatures - Domain coreFeatures object
* @returns {boolean}
*/
function matchesDomainFeature(generatedFeatureTitle, domainFeatures) {
const g = normalize(generatedFeatureTitle);
const allDomainFeatureNames = Object.values(domainFeatures).flat();
return allDomainFeatureNames.some(
(dfn) =>
normalize(dfn) === g ||
normalize(dfn).includes(g) ||
g.includes(normalize(dfn))
);
}
// ══════════════════════════════════════════════════════════
// Public API
// ══════════════════════════════════════════════════════════
/**
* Validate a requirement package against a target domain.
*
* @param {object} requirementPackage - The requirement package to validate
* @param {object[]} [requirementPackage.epics] - Generated epics
* @param {object[]} [requirementPackage.features] - Generated features
* @param {object[]} [requirementPackage.userStories] - Generated user stories
* @param {string} domainName - Target domain name for validation
* @returns {ValidationReport}
*
* @example
* const report = validateRequirements({ epics, features, userStories }, "MES");
* // => { coverageScore: 80, consistencyScore: 75, completenessScore: 70, ... }
*/
export function validateRequirements(requirementPackage, domainName) {
// ── Default empty result ──────────────────────────────
const emptyResult = {
coverageScore: 0,
consistencyScore: 0,
completenessScore: 0,
missingEpics: [],
extraEpics: [],
recommendations: [],
overallVerdict: "fail",
};
if (!requirementPackage || typeof requirementPackage !== "object") {
return {
...emptyResult,
recommendations: ["Requirement package is empty or invalid"],
};
}
if (!domainName) {
return {
...emptyResult,
recommendations: ["No domain specified for validation"],
};
}
const domain = getDomain(domainName);
if (!domain) {
return {
...emptyResult,
recommendations: [
`Domain "${domainName}" not found in registry. Available: ${listDomains().join(", ")}`,
],
};
}
// Extract inputs with defaults
const generatedEpics = requirementPackage.epics || [];
const generatedFeatures = requirementPackage.features || [];
const generatedUserStories = requirementPackage.userStories || [];
const domainEpics = domain.coreEpics || [];
const domainFeatures = domain.coreFeatures || {};
// ══ 1. Completeness: % of domain coreEpics present ═══
const coveredDomainTitles = domainEpics.filter((de) =>
matchesDomainEpic(de.title, generatedEpics)
);
const missingEpics = domainEpics
.filter((de) => !matchesDomainEpic(de.title, generatedEpics))
.map((de) => de.title);
const completenessScore =
domainEpics.length > 0
? Math.round((coveredDomainTitles.length / domainEpics.length) * 100)
: 0;
// ══ 2. Coverage: % of generated epics matching domain ═══
const coveredGeneratedEpics = generatedEpics.filter((ge) =>
matchesDomainEpic(ge.title, domainEpics)
);
const extraEpics = generatedEpics
.filter((ge) => !matchesDomainEpic(ge.title, domainEpics))
.map((ge) => ge.title);
const coverageScore =
generatedEpics.length > 0
? Math.round((coveredGeneratedEpics.length / generatedEpics.length) * 100)
: 0;
// ══ 3. Consistency: features matching domain ════════
const consistentFeatures = generatedFeatures.filter((gf) =>
matchesDomainFeature(gf.title, domainFeatures)
);
const consistencyScore =
generatedFeatures.length > 0
? Math.round((consistentFeatures.length / generatedFeatures.length) * 100)
: 100; // If no features, consider it consistent (trivially)
// ══ 4. Generate recommendations ═══════════════════════
const recommendations = [];
if (missingEpics.length > 0) {
recommendations.push(
`Add missing epics from ${domain.displayName}: ${missingEpics.join(", ")}`
);
}
if (extraEpics.length > 0) {
recommendations.push(
`Remove or align extra epics not in ${domain.displayName}: ${extraEpics.join(", ")}`
);
}
if (consistencyScore < 70 && generatedFeatures.length > 0) {
recommendations.push(
`Feature consistency is low (${consistencyScore}%). Review feature titles against ${domain.displayName} core features.`
);
}
if (completenessScore < 50) {
recommendations.push(
`Completeness is critically low (${completenessScore}%). Consider increasing domain epic coverage for ${domain.displayName}.`
);
}
// Recommendations for user stories (if available)
if (generatedUserStories.length > 0 && generatedEpics.length > 0) {
const storyCountPerEpic = generatedUserStories.filter((s) => s.epicId).length;
if (storyCountPerEpic < generatedEpics.length) {
recommendations.push(
"Ensure each epic has at least one associated user story."
);
}
}
if (generatedEpics.length === 0) {
recommendations.push(
`No epics generated. Start by adding epics for ${domain.displayName}.`
);
}
if (!recommendations.length) {
recommendations.push(
`${domain.displayName} requirement package looks well-aligned with domain expectations.`
);
}
// ══ 5. Overall verdict ═══════════════════════════════
const overallVerdict = computeVerdict(completenessScore, coverageScore, consistencyScore, missingEpics.length);
return {
coverageScore,
consistencyScore,
completenessScore,
missingEpics,
extraEpics,
recommendations,
overallVerdict,
};
}
/**
* Compute the overall verdict based on component scores.
*
* @param {number} completeness - Completeness score 0-100
* @param {number} coverage - Coverage score 0-100
* @param {number} consistency - Consistency score 0-100
* @param {number} missingCount - Number of missing epics
* @returns {"pass"|"conditional"|"fail"}
*/
function computeVerdict(completeness, coverage, consistency, missingCount) {
// Fail: critically missing domain content
if (completeness < 30 || missingCount >= 5) return "fail";
// Pass: all scores high
if (completeness >= 80 && coverage >= 80 && consistency >= 80) return "pass";
// Conditional: some gaps but not critical
if (completeness >= 30) return "conditional";
return "fail";
}
/**
* Validate an epic array alone against a domain (lightweight).
*
* @param {object[]} epics - Generated epics
* @param {string} domainName - Target domain
* @returns {ValidationReport}
*/
export function validateEpics(epics, domainName) {
return validateRequirements({ epics, features: [], userStories: [] }, domainName);
}
/**
* Validate features alone against a domain (lightweight).
*
* @param {object[]} features - Generated features
* @param {string} domainName - Target domain
* @returns {object} Partial validation result with consistencyScore
*/
export function validateFeatures(features, domainName) {
const result = validateRequirements(
{ epics: [], features, userStories: [] },
domainName
);
return {
consistencyScore: result.consistencyScore,
consistencyDetails: `Feature consistency with ${domainName}: ${result.consistencyScore}%`,
recommendations: result.recommendations.filter((r) =>
r.toLowerCase().includes("feature") || r.toLowerCase().includes("consistency")
),
};
}