Files
ECC/scripts/lib/context-pack-registry.js

161 lines
7.4 KiB
JavaScript

'use strict';
const fs = require('fs');
const path = require('path');
const yaml = require('js-yaml');
const {
DEFAULT_REPO_ROOT, TARGETS, createSourceReader, digestObject, validateRelativePath,
isExcludedResource, normalizeMetadataText, validateSchema, validateTarget,
} = require('./context-profile-support');
const REGISTRY_PATH = 'manifests/context-packs/skill-registry@1.json';
const TRIGGERS_PATH = 'manifests/context-packs/skill-triggers@1.json';
const ID_PATTERN = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
function validateModules(document) {
if (!document || !Array.isArray(document.modules)) throw new Error('Install source requires a modules array');
const ids = new Set();
for (const module of document.modules) {
if (!module || !ID_PATTERN.test(module.id)) throw new Error('Invalid install module ID');
if (ids.has(module.id)) throw new Error(`Duplicate install module ID: ${module.id}`);
ids.add(module.id);
if (!Array.isArray(module.paths) || !Array.isArray(module.targets)) throw new Error(`Invalid module paths or targets: ${module.id}`);
module.paths.forEach(validateRelativePath);
module.targets.forEach(validateTarget);
}
return document.modules;
}
function discoverSkills(reader, root) {
return reader.list(root).filter(name => {
const skillRoot = `${root}/${name}`;
if (isExcludedResource(skillRoot)) return false;
const absolute = reader.resolve(skillRoot);
if (!fs.statSync(absolute).isDirectory()) return false;
if (!ID_PATTERN.test(name)) throw new Error(`Invalid canonical skill ID: ${name}`);
return reader.list(skillRoot).includes('SKILL.md');
});
}
function parseMetadata(resource) {
const source = resource.content.toString('utf8').replace(/^\uFEFF/, '').replace(/\r\n?/g, '\n');
const match = source.match(/^---\n([\s\S]*?)\n---(?:\n|$)/);
if (!match) throw new Error(`Missing skill metadata: ${resource.path}`);
let metadata;
try { metadata = yaml.load(match[1], { schema: yaml.JSON_SCHEMA }); } catch (error) {
throw new Error(`Invalid skill metadata: ${resource.path}: ${error.message}`);
}
return Object.fromEntries(['name', 'description'].map(key => [
key, normalizeMetadataText(metadata && metadata[key], `Skill ${key} (${resource.path})`),
]));
}
function indexedOverrides(overrides, ids) {
const byId = new Map();
for (const override of overrides) {
if (!ids.has(override.id)) throw new Error(`Unknown override ID: ${override.id}`);
if (byId.has(override.id)) throw new Error(`Duplicate override ID: ${override.id}`);
byId.set(override.id, override);
}
return byId;
}
function validateDependencies(entries) {
const byId = new Map(entries.map(entry => [entry.id, entry]));
const visited = new Set();
const visiting = new Set();
function visit(id) {
if (visited.has(id)) return;
if (visiting.has(id)) throw new Error(`Dependency cycle at ${id}`);
visiting.add(id);
for (const dependency of byId.get(id).dependencies) {
if (!byId.has(dependency)) throw new Error(`Unknown dependency ${dependency} for ${id}`);
visit(dependency);
}
visiting.delete(id);
visited.add(id);
}
entries.forEach(entry => visit(entry.id));
}
function buildEntry(reader, modules, root, name, override = {}) {
const skillRoot = `${root}/${name}`;
const sourcePath = `${skillRoot}/SKILL.md`;
const owners = modules.filter(module => module.paths.some(source => sourcePath === source || sourcePath.startsWith(`${source}/`)));
if (owners.length !== 1) throw new Error(`Skill ${name} requires exactly one owner; found ${owners.length}`);
for (const resource of override.requiredResources || []) {
validateRelativePath(resource);
if (!resource.startsWith(`${skillRoot}/`)) throw new Error(`Required resource must belong to ${skillRoot}`);
if (isExcludedResource(resource)) throw new Error(`Required resource is excluded from publication: ${resource}`);
reader.read(resource);
}
const metadata = parseMetadata(reader.read(sourcePath));
const resources = reader.walk(skillRoot).map(({ path: resourcePath, digest, bytes }) => ({
path: resourcePath, digest, bytes,
}));
return {
id: `skill:${name}`, kind: 'skill', sourcePath, ...metadata,
ownerModuleId: owners[0].id, packId: owners[0].id,
declaredInstallTargets: [...new Set(owners[0].targets)].sort(),
dependencies: [...(override.dependencies || [])].sort(),
requiredResources: [...(override.requiredResources || [])].sort(),
dependencyCoverage: 'declared-only-unreviewed',
resources, contentDigest: digestObject(resources),
};
}
function loadContextRegistry({ repoRoot = DEFAULT_REPO_ROOT } = {}) {
const reader = createSourceReader(repoRoot);
const manifest = reader.json(REGISTRY_PATH);
validateSchema(manifest, 'context-pack-registry.schema.json');
const modules = validateModules(reader.json(manifest.inventory.source));
const names = discoverSkills(reader, manifest.inventory.skillsRoot);
const overrides = indexedOverrides(manifest.overrides, new Set(names.map(name => `skill:${name}`)));
const entries = names.map(name => buildEntry(reader, modules, manifest.inventory.skillsRoot, name, overrides.get(`skill:${name}`)));
validateDependencies(entries);
const value = {
schemaVersion: 'ecc.context-registry.v1', id: manifest.id,
sourceDigests: [REGISTRY_PATH, manifest.inventory.source].map(source => ({ path: source, digest: reader.read(source).digest })),
targets: [...TARGETS],
packs: [...new Set(entries.map(entry => entry.packId))].sort().map(id => ({ id })),
entries,
excludedSurfaces: ['agents', 'commands', 'rules', 'hooks', 'mcp-schemas', 'harness-wrappers', 'learned-skills'],
limitations: ['Only canonical skill discovery is inventoried.', 'Dependency declarations are incomplete until explicitly reviewed.', 'Aliases and capability activation are outside this schema.'],
};
return { ...value, registryDigest: digestObject(value) };
}
function loadSkillTriggers({ repoRoot = DEFAULT_REPO_ROOT } = {}) {
const file = path.join(repoRoot, TRIGGERS_PATH);
if (!fs.existsSync(file) || !fs.statSync(file).isFile()) return { triggers: {}, manifest: null };
let manifest;
try { manifest = JSON.parse(fs.readFileSync(file, 'utf8')); }
catch (error) { throw new Error(`Invalid skill triggers manifest: ${error.message}`); }
if (!manifest || manifest.schemaVersion !== 1 || !manifest.triggers || typeof manifest.triggers !== 'object') {
throw new Error('Invalid skill triggers manifest: expected schemaVersion 1 with a triggers object');
}
const triggers = {};
for (const [id, list] of Object.entries(manifest.triggers)) {
if (!Array.isArray(list) || !list.length) continue;
triggers[id] = [...new Set(list.map(item => String(item).trim().toLowerCase()).filter(Boolean))];
}
return { triggers, manifest };
}
function projectionFor(entry, target) {
return {
installSupport: entry.declaredInstallTargets.includes(target) ? 'declared' : 'not-declared',
nativeSupport: 'unobserved',
};
}
function explainContextEntry({ repoRoot = DEFAULT_REPO_ROOT, id, target = 'codex' } = {}) {
validateTarget(target);
const registry = loadContextRegistry({ repoRoot });
const entry = registry.entries.find(value => value.id === id);
if (!entry) throw new Error(`Unknown context entry: ${id}`);
return { ...entry, target, projection: projectionFor(entry, target), registryDigest: registry.registryDigest };
}
module.exports = { explainContextEntry, loadContextRegistry, loadSkillTriggers, projectionFor };