140 lines
3.9 KiB
JavaScript
140 lines
3.9 KiB
JavaScript
// Curated doc library — the allow-list for recommender citations.
|
|
|
|
import { readFile } from 'node:fs/promises';
|
|
import { fileURLToPath } from 'node:url';
|
|
import { dirname, join } from 'node:path';
|
|
|
|
const HERE = dirname(fileURLToPath(import.meta.url));
|
|
const LIBRARY_PATH = join(HERE, '..', 'references', 'docs-library.json');
|
|
|
|
let cached;
|
|
|
|
export async function loadLibrary() {
|
|
if (cached) return cached;
|
|
const raw = await readFile(LIBRARY_PATH, 'utf-8');
|
|
cached = JSON.parse(raw);
|
|
return cached;
|
|
}
|
|
|
|
export async function isKnownUrl(url) {
|
|
const lib = await loadLibrary();
|
|
return lib.urls.some(e => e.url === url);
|
|
}
|
|
|
|
export async function lookupUrl(url) {
|
|
const lib = await loadLibrary();
|
|
return lib.urls.find(e => e.url === url);
|
|
}
|
|
|
|
export async function lookupSkillRule(ref) {
|
|
const lib = await loadLibrary();
|
|
const m = ref.match(/^([\w-]+):([\w-]+)$/);
|
|
if (!m) return undefined;
|
|
return lib.ruleSkillRefs.find(r => r.skill === m[1] && r.rule === m[2]);
|
|
}
|
|
|
|
// Narrow semver subset: "*", "fw@*", "fw@14", "fw@>=15.0.0", "fw@<X", "fw@X.Y", "fw@X.Y.Z", "a || b".
|
|
export function matchesFrameworkVersion(pattern, framework, version) {
|
|
if (pattern === '*') return true;
|
|
|
|
if (pattern.includes('||')) {
|
|
return pattern.split('||').map(p => p.trim()).some(p =>
|
|
matchesFrameworkVersion(p, framework, version)
|
|
);
|
|
}
|
|
|
|
const m = pattern.match(/^([\w-]+)@(.+)$/);
|
|
if (!m) return false;
|
|
const [, fw, range] = m;
|
|
|
|
if (fw !== framework) return false;
|
|
if (range === '*') return true;
|
|
|
|
const verParts = parseVersion(version);
|
|
if (!verParts) return false;
|
|
|
|
let m2 = range.match(/^>=\s*(.+)$/);
|
|
if (m2) {
|
|
const min = parseVersion(m2[1]);
|
|
return min ? compareVersion(verParts, min) >= 0 : false;
|
|
}
|
|
|
|
m2 = range.match(/^<\s*(.+)$/);
|
|
if (m2) {
|
|
const max = parseVersion(m2[1]);
|
|
return max ? compareVersion(verParts, max) < 0 : false;
|
|
}
|
|
|
|
if (/^\d+$/.test(range)) {
|
|
return verParts[0] === Number(range);
|
|
}
|
|
|
|
m2 = range.match(/^(\d+)\.(\d+)$/);
|
|
if (m2) {
|
|
return verParts[0] === Number(m2[1]) && verParts[1] === Number(m2[2]);
|
|
}
|
|
|
|
const exact = parseVersion(range);
|
|
if (exact) return compareVersion(verParts, exact) === 0;
|
|
|
|
return false;
|
|
}
|
|
|
|
function parseVersion(v) {
|
|
const m = String(v).replace(/^[v^~]+/, '').match(/^(\d+)(?:\.(\d+))?(?:\.(\d+))?/);
|
|
if (!m) return null;
|
|
return [Number(m[1]) || 0, Number(m[2]) || 0, Number(m[3]) || 0];
|
|
}
|
|
|
|
function compareVersion(a, b) {
|
|
for (let i = 0; i < 3; i++) {
|
|
if (a[i] !== b[i]) return a[i] - b[i];
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
// Filtered subset embedded in recommender prompt — LLM never sees URLs for features not in user's stack.
|
|
export async function libraryForStack(framework, version) {
|
|
const lib = await loadLibrary();
|
|
const matches = (frameworks) =>
|
|
frameworks.some(p => matchesFrameworkVersion(p, framework, version) || p === '*');
|
|
return {
|
|
urls: lib.urls.filter(e => matches(e.applicableFrameworks)),
|
|
ruleSkillRefs: lib.ruleSkillRefs.filter(r => matches(r.applicableFrameworks)),
|
|
};
|
|
}
|
|
|
|
export async function sanitizeCitations(rec, framework, version) {
|
|
const lib = await loadLibrary();
|
|
const strippedUnknown = [];
|
|
const strippedVersion = [];
|
|
const kept = [];
|
|
|
|
for (const cite of rec.citations ?? []) {
|
|
const ruleRef = await lookupSkillRule(cite);
|
|
if (ruleRef) {
|
|
if (matchesFrameworkVersion(ruleRef.applicableFrameworks.join(' || '), framework, version) || ruleRef.applicableFrameworks.includes('*')) {
|
|
kept.push(cite);
|
|
} else {
|
|
strippedVersion.push(cite);
|
|
}
|
|
continue;
|
|
}
|
|
|
|
const entry = lib.urls.find(e => e.url === cite);
|
|
if (!entry) {
|
|
strippedUnknown.push(cite);
|
|
continue;
|
|
}
|
|
if (entry.applicableFrameworks.includes('*') ||
|
|
entry.applicableFrameworks.some(p => matchesFrameworkVersion(p, framework, version))) {
|
|
kept.push(cite);
|
|
} else {
|
|
strippedVersion.push(cite);
|
|
}
|
|
}
|
|
|
|
rec.citations = kept;
|
|
return { rec, strippedUnknown, strippedVersion };
|
|
}
|